Проектная работа №2: "Рыночный риск"¶
Выполнили: Ткалич Леонид, Шашмединов Илья, Шурыгин Всеволод и Шумилин Андрей
Источники¶
investing.com- котировки акций и индексовcbr.ru- курсы валютcbonds.ru- ставки на разные промежутки, котировки облигаций и даты выплаты купоновdohod.ru- списки облигаций
Содержание¶
Для навигации по содержанию необходимо кликнуть на интересующий раздел. Чтобы вернуться в содержание после клика на раздел, можно кликнуть снова на название раздела.
- 1. Загрузка данных
- 2. Анализ риск-факторов
- 3. Стохастическая модель динамики
- 4. Оценка справедливой стоимости
- 5. Оценка риска по портфелю
- 6-7. Backtesting
path_data = 'data/'
start = '2021-01-01'
end = '2025-01-01'
period_start = '2023-01-01'
period_end = '2025-01-01'
import os
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from matplotlib.ticker import AutoMinorLocator
import plotly.graph_objects as go
from statsmodels.tsa.seasonal import seasonal_decompose
from statsmodels.tsa.stattools import adfuller
from statsmodels.tsa.stattools import acf, pacf
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
from scipy.stats import ttest_1samp
pd.set_option('display.max_rows', 150)
pd.set_option('display.max_columns', None)
def make_ax_better(ax, locators=()):
"""
Функция добавляет сетку, убирает края и делает minor ticks
"""
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
if 'x' in locators:
ax.xaxis.set_minor_locator(AutoMinorLocator())
if 'y' in locators:
ax.yaxis.set_minor_locator(AutoMinorLocator())
if locators:
ax.tick_params(which='minor', length=2.5)
ax.tick_params(which='major', length=5)
ax.grid(which='minor', linewidth=0.15, color='tab:grey', alpha=0.3)
ax.grid(linewidth=0.5, color='tab:grey', alpha=0.3)
ax.set_axisbelow(True)
def make_str_bold(s):
"""
Функция для выделения строки жирным для print
"""
return '\033[1m' + str(s) + '\033[0m'
def to_bold(s):
s = ' '.join([r"$\bf{" + str(item) + "}$" for item in s.split(' ')])
return s.replace('_', '}$_$\\bf{')
colors = [
'#42CAFD', '#FF751F', '#8DD65C', '#FF495C',
'#D68FD6',
'#FFCB47',
'#DACC3E', '#CC5A71', '#A44200',
'#42CAFD', '#FF751F', '#8DD65C', '#FF495C',
'#42CAFD', '#D68FD6', '#FFCB47',
] * 10
def plot_ts_plotly(
df: pd.DataFrame,
x: str,
y: list[str],
palette: list[str] = colors,
title: str = None,
xaxis_title: str = 'Дата',
yaxis_title: str = '% от цены покупки',
fig_size: tuple[int, int] = (1100, 550), # Размер фигуры (ширина, высота)
):
# Создаем фигуру
fig = go.Figure()
# Добавляем линии для каждого столбца в DataFrame
for col, color in zip(y, palette):
fig.add_trace(
go.Scatter(
x=df[x],
y=df[col],
mode='lines',
name=col,
line=dict(color=color),
hovertemplate=f'<b>{col}</b>: %{{y:.1f}}%<extra></extra>'
)
)
# Настраиваем заголовок и оси
fig.update_layout(
title={
'text': title,
'y': 0.95, # Позиция заголовка по вертикали
'x': 0.5, # Позиция заголовка по горизонтали (центр)
'xanchor': 'center', # Центрируем заголовок
'yanchor': 'top', # Привязка к верхней части
'font': dict(size=22) # Размер шрифта заголовка
},
xaxis_title=xaxis_title,
yaxis_title=yaxis_title,
template='plotly_white',
legend=dict(
x=0.5, # Центрируем легенду по горизонтали
y=-0.2, # Размещаем легенду ниже графика
xanchor='center', # Привязка к центру
yanchor='top', # Привязка к верхней части
orientation='h', # Горизонтальная ориентация
font=dict(size=12), # Размер шрифта легенды
traceorder='normal', # Порядок элементов легенды
itemwidth=50, # Ширина элемента легенды
itemsizing='constant', # Фиксированный размер элементов
bordercolor='lightgray', # Цвет границы легенды
borderwidth=1, # Ширина границы легенды
bgcolor='rgba(255, 255, 255, 0.8)', # Цвет фона легенды
# columns=legend_cols # Количество столбцов в легенде
),
hovermode='x unified',
width=fig_size[0], # Ширина фигуры
height=fig_size[1], # Высота фигуры
margin=dict(l=50, r=50, b=50, t=100) # Отступы (left, right, bottom, top)
)
# Настраиваем оси
fig.update_xaxes(
showgrid=True,
gridwidth=1,
gridcolor='lightgray',
minor=dict(
ticklen=4, # Длина minor-тиков
tickcolor='gray', # Цвет minor-тиков
showgrid=True, # Показываем minor-сетку
gridcolor='rgba(211, 211, 211, 0.5)', # Цвет minor-сетки (светло-серый с прозрачностью)
griddash='dot' # Стиль minor-сетки (точечный)
)
)
fig.update_yaxes(
showgrid=True,
gridwidth=1,
gridcolor='lightgray',
minor=dict(
ticklen=4, # Длина minor-тиков
tickcolor='gray', # Цвет minor-тиков
showgrid=True, # Показываем minor-сетку
gridcolor='rgba(211, 211, 211, 0.5)', # Цвет minor-сетки (светло-серый с прозрачностью)
griddash='dot' # Стиль minor-сетки (точечный)
)
)
# Показываем график
fig.show()
def plot_decomposed_ts(
ts: pd.Series,
model: str = 'additive',
color: str = 'tab:blue',
axes = None,
**kwargs
):
decomposed = seasonal_decompose(ts, model=model, extrapolate_trend='freq', **kwargs)
decomposed = pd.concat([decomposed.observed, decomposed.seasonal, decomposed.trend, decomposed.resid], axis=1)
if axes is None:
fig, axes = plt.subplots(figsize=(15, 13), nrows=4, dpi=150)
col_name = decomposed.columns[0]
for col, ax in zip(decomposed, axes):
sns.lineplot(decomposed[col], ax=ax, color=color)
make_ax_better(ax, locators=['x', 'y'])
title = f'{col_name} ({col})' if col != col_name else col_name
ax.set_title(title, fontsize=22)
ax.set_xlabel('')
ax.set_xlim(decomposed.index.min() - pd.DateOffset(days=7), decomposed.index.max() + pd.DateOffset(days=7))
plt.tight_layout(h_pad=0.7)
def preprocess_data(df):
df = df[
(df.index >= pd.Timestamp(start))
& (df.index < pd.Timestamp(end))
].reset_index('Дата').copy()
dates = pd.DataFrame({'Дата': pd.date_range(start, end, inclusive='left')})
df = pd.merge(
left=dates,
right=df,
how='left'
)
df = (
df
.ffill()
.bfill()
.set_index('Дата')
)
return df
share_names = [
'SBER',
'YDEX',
'ROSN',
'PLZL',
'LKOH',
'GAZP',
'NVTK',
'MOEX',
'CHMF',
'GMKN'
]
stocks = []
for ticker in share_names:
filepath = path_data + ticker + '.csv'
cur_df = pd.read_csv(filepath, usecols=['Дата', 'Цена']).drop_duplicates()
cur_df['Дата'] = pd.to_datetime(cur_df['Дата'], format='%d.%m.%Y')
cur_df['Цена'] = cur_df['Цена'].astype(str).str.rstrip('.0').str.replace('.', '').str.replace(',', '.').astype(float)
n_duplicates = cur_df.shape[0] - cur_df['Дата'].nunique()
if n_duplicates:
print(filepath, f'число дубликатов: {n_duplicates}, усредняем цены за дублирующиеся даты')
cur_df = cur_df.groupby(['Дата'], as_index=False)['Цена'].mean()
cur_df = cur_df.set_index('Дата').rename(columns={'Цена': ticker})
stocks.append(cur_df)
stocks = pd.concat(stocks, axis=1)
print(stocks.shape)
stocks = preprocess_data(stocks)
stocks.tail()
data/SBER.csv число дубликатов: 3, усредняем цены за дублирующиеся даты data/GAZP.csv число дубликатов: 2, усредняем цены за дублирующиеся даты (1000, 10)
| SBER | YDEX | ROSN | PLZL | LKOH | GAZP | NVTK | MOEX | CHMF | GMKN | |
|---|---|---|---|---|---|---|---|---|---|---|
| Дата | ||||||||||
| 2024-12-27 | 271.20 | 3848.0 | 591.00 | 14139.0 | 6990.5 | 127.79 | 949.2 | 192.45 | 1186.2 | 111.0 |
| 2024-12-28 | 272.83 | 3928.5 | 596.00 | 13926.0 | 6998.0 | 129.60 | 951.8 | 192.61 | 1232.2 | 113.8 |
| 2024-12-29 | 272.83 | 3928.5 | 596.00 | 13926.0 | 6998.0 | 129.60 | 951.8 | 192.61 | 1232.2 | 113.8 |
| 2024-12-30 | 279.43 | 3994.0 | 606.05 | 13981.0 | 7235.0 | 133.12 | 996.0 | 199.22 | 1337.4 | 115.5 |
| 2024-12-31 | 279.43 | 3994.0 | 606.05 | 13981.0 | 7235.0 | 133.12 | 996.0 | 199.22 | 1337.4 | 115.5 |
df = stocks.copy()
df = (df / df.iloc[0] - 1) * 100
cols_to_show = ['1W', '1M', '1Y', '5Y', '10Y', '30Y']
plot_ts_plotly(
df.reset_index(),
x='Дата',
y=df.columns,
title='Котировки рассматриваемых акций за период в % от стоимости на начало периода',
xaxis_title='Дата',
yaxis_title='% от стоимости на начало периода'
)
rates = pd.read_excel(path_data + 'rates.xlsx')
rates['Дата'] = pd.to_datetime(rates['Дата'], format='%d.%m.%Y')
rates = (
rates
.rename(columns={
col: col.split()[-1] for col in rates.columns[1:]
})
.set_index('Дата')
.drop(columns=['РФ'])
)
rates = preprocess_data(rates)
rates.tail()
| 10Y | 15Y | 20Y | 25Y | 30Y | 1Y | 2Y | 3Y | 5Y | 7Y | 4Y | 8Y | 9Y | 1W | 2W | 1M | 2M | 3M | 4M | 9M | 6M | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Дата | |||||||||||||||||||||
| 2024-12-27 | 15.430508 | 14.747656 | 14.363036 | 14.137707 | 13.990107 | 17.669109 | 17.561862 | 17.292200 | 16.649654 | 16.078685 | 16.970190 | 15.835069 | 15.620094 | 17.355953 | 17.368659 | 17.396492 | 17.444275 | 17.488220 | 17.523689 | 17.643571 | 17.585694 |
| 2024-12-28 | 15.566986 | 14.916261 | 14.550008 | 14.337680 | 14.200138 | 18.525025 | 18.151565 | 17.671248 | 16.828921 | 16.210946 | 17.217608 | 15.964092 | 15.751326 | 18.486078 | 18.493498 | 18.509232 | 18.534271 | 18.554448 | 18.567988 | 18.569813 | 18.582141 |
| 2024-12-29 | 15.566986 | 14.916261 | 14.550008 | 14.337680 | 14.200138 | 18.525025 | 18.151565 | 17.671248 | 16.828921 | 16.210946 | 17.217608 | 15.964092 | 15.751326 | 18.486078 | 18.493498 | 18.509232 | 18.534271 | 18.554448 | 18.567988 | 18.569813 | 18.582141 |
| 2024-12-30 | 15.222626 | 14.573456 | 14.220410 | 14.024297 | 13.897065 | 18.581608 | 18.055996 | 17.481006 | 16.533707 | 15.873952 | 16.964345 | 15.620923 | 15.406795 | 18.802057 | 18.803098 | 18.804450 | 18.803235 | 18.797023 | 18.787161 | 18.679350 | 18.753506 |
| 2024-12-31 | 15.222626 | 14.573456 | 14.220410 | 14.024297 | 13.897065 | 18.581608 | 18.055996 | 17.481006 | 16.533707 | 15.873952 | 16.964345 | 15.620923 | 15.406795 | 18.802057 | 18.803098 | 18.804450 | 18.803235 | 18.797023 | 18.787161 | 18.679350 | 18.753506 |
cols_to_show = ['1W', '1M', '1Y', '5Y', '10Y', '30Y']
plot_ts_plotly(
rates.reset_index(),
x='Дата',
y=cols_to_show,
title='Процентные ставки в динамике'
)
coupons = []
bonds = []
bonds_path = path_data + '/bonds/'
for filename in os.listdir(bonds_path):
if not filename.startswith('~') and filename != '.DS_Store':
series, isin, file_type = filename.split('_')
file_type = file_type.split('.')[0]
if file_type == 'coupons':
file = pd.read_excel(bonds_path + filename, skiprows=2)
file['ISIN'] = isin
coupons.append(file)
elif file_type == 'price':
file = pd.read_excel(bonds_path + filename, skiprows=1)
bonds.append(file)
else:
raise ValueError('Unknow file type')
bonds = pd.concat(bonds)
bonds['Дата'] = pd.to_datetime(pd.to_datetime(bonds['Дата']).dt.date)
bonds = bonds.set_index('Дата')
bonds = preprocess_data(bonds)
bond_prices = pd.pivot(
bonds.reset_index(),
index='Дата',
values='Indicative',
columns=['ISIN']
) * 1000 / 100
bond_prices = preprocess_data(bond_prices)
coupons = pd.concat(coupons)
display(bonds.head())
display(coupons.head())
| Биржа | Bid | Ask | Indicative | YTM Bid | YTM Ask | YTM Indicative | Оборот | G-spread | ISIN | Рег. номер | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Дата | |||||||||||
| 2021-01-01 | Московская биржа Т+ | 100.013 | 100.449 | 100.005 | 5.9848 | 5.9258 | 5.9859 | 1541069.57 | 3.738296 | RU000A1028E3 | 26235RMFS |
| 2021-01-02 | Московская биржа Т+ | 100.013 | 100.449 | 100.005 | 5.9848 | 5.9258 | 5.9859 | 1541069.57 | 3.738296 | RU000A1028E3 | 26235RMFS |
| 2021-01-03 | Московская биржа Т+ | 100.013 | 100.449 | 100.005 | 5.9848 | 5.9258 | 5.9859 | 1541069.57 | 3.738296 | RU000A1028E3 | 26235RMFS |
| 2021-01-04 | Московская биржа Т+ | 100.013 | 100.449 | 100.005 | 5.9848 | 5.9258 | 5.9859 | 1541069.57 | 3.738296 | RU000A1028E3 | 26235RMFS |
| 2021-01-04 | Московская биржа Т+ | 96.930 | 96.990 | 96.854 | 5.3394 | 5.3236 | 5.3593 | 7270843.45 | 16.401313 | RU000A101QE0 | 26234RMFS |
| № | Окончание купона | Фактическая выплата | Фиксация списка держателей | Купон, % | Сумма купона RUB | Погашение RUB | ISIN | |
|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 2021-03-24 | 2021-03-24 | 2021-03-23 | 5.9 | 26.02 | NaN | RU000A1028E3 |
| 1 | 2 | 2021-09-22 | 2021-09-22 | 2021-09-21 | 5.9 | 29.42 | NaN | RU000A1028E3 |
| 2 | 3 | 2022-03-23 | 2022-03-23 | 2022-03-22 | 5.9 | 29.42 | NaN | RU000A1028E3 |
| 3 | 4 | 2022-09-21 | 2022-09-21 | 2022-09-20 | 5.9 | 29.42 | NaN | RU000A1028E3 |
| 4 | 5 | 2023-03-22 | 2023-03-22 | 2023-03-21 | 5.9 | 29.42 | NaN | RU000A1028E3 |
# Курсы цб
usdrub = pd.read_csv(path_data + 'usd.csv')
eurrub = pd.read_csv(path_data + 'eur.csv')
# Инвестинг
imoex = pd.read_csv(path_data + 'IMOEX.csv')
irts = pd.read_csv(path_data + 'IRTS.csv')
brent = pd.read_csv(path_data + 'brent.csv')
gold = pd.read_csv(path_data + 'gold.csv')
df_index = []
for df, name in zip([usdrub, eurrub], ['usd', 'eur']):
df = df.rename(columns={'data': 'Дата', 'curs': name})
df['Дата'] = pd.to_datetime(df['Дата'])
df_index.append(df.set_index('Дата')[[name]])
for df, name in zip([imoex, irts, brent, gold], ['imoex', 'irts', 'brent', 'gold']):
df['Дата'] = pd.to_datetime(df['Дата'], format='%d.%m.%Y')
df['Цена'] = (
df['Цена']
.astype(str)
.str.rstrip('.0')
.str.replace('.', '')
.str.replace(',', '.')
.astype(float)
)
df = (
df
.rename(columns={'Цена': name})
.set_index('Дата')
[[name]]
)
df_index.append(df)
df_index = pd.concat(df_index, axis=1)
df_index = preprocess_data(df_index)
df_index.head()
| usd | eur | imoex | irts | brent | gold | |
|---|---|---|---|---|---|---|
| Дата | ||||||
| 2021-01-01 | 73.8757 | 90.7932 | 3350.51 | 1424.84 | 51.09 | 1898.10 |
| 2021-01-02 | 73.8757 | 90.7932 | 3350.51 | 1424.84 | 51.09 | 1898.10 |
| 2021-01-03 | 73.8757 | 90.7932 | 3350.51 | 1424.84 | 51.09 | 1898.10 |
| 2021-01-04 | 73.8757 | 90.7932 | 3350.51 | 1424.84 | 51.09 | 1942.28 |
| 2021-01-05 | 73.8757 | 90.7932 | 3359.15 | 1426.11 | 53.60 | 1949.35 |
df = df_index.copy()
df = (df / df.iloc[0] - 1) * 100
cols_to_show = ['1W', '1M', '1Y', '5Y', '10Y', '30Y']
plot_ts_plotly(
df.reset_index(),
x='Дата',
y=df.columns,
title='Валюты, индексы и сырье',
xaxis_title='Дата',
yaxis_title='% от стоимости на начало периода'
)
all_data = pd.concat([
df_index,
rates,
stocks,
bond_prices
], axis=1)
# Фильтруем по датам
all_data = all_data[
(all_data.index >= pd.Timestamp(period_start))
& (all_data.index < pd.Timestamp(period_end))
& (all_data.index.weekday < 5) # Исключаем выходные
]
# Делаем разность
all_data_diff = all_data.copy()
for col in all_data.columns:
if col in rates.columns:
all_data_diff[col] = all_data_diff[col].diff()
else:
all_data_diff[col] = all_data_diff[col].pct_change(fill_method=None)
all_data.shape
(522, 42)
all_data_diff.corr()
| usd | eur | imoex | irts | brent | gold | 10Y | 15Y | 20Y | 25Y | 30Y | 1Y | 2Y | 3Y | 5Y | 7Y | 4Y | 8Y | 9Y | 1W | 2W | 1M | 2M | 3M | 4M | 9M | 6M | SBER | YDEX | ROSN | PLZL | LKOH | GAZP | NVTK | MOEX | CHMF | GMKN | RU000A0JS3W6 | RU000A0ZYUA9 | RU000A100EF5 | RU000A101QE0 | RU000A1028E3 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| usd | 1.000000 | 0.875312 | 0.015139 | -0.108508 | 0.027208 | -0.080204 | -0.033898 | -0.036629 | -0.031181 | -0.025370 | -0.020474 | 0.076908 | 0.078563 | 0.057461 | 0.010729 | -0.016453 | 0.031959 | -0.024352 | -0.030006 | 0.043817 | 0.044945 | 0.054935 | 0.060463 | 0.065105 | 0.068235 | 0.074613 | 0.072082 | 0.003562 | -0.051122 | 0.025896 | -0.062618 | 0.010914 | 0.008987 | 0.034316 | -0.100134 | -0.046182 | -0.030118 | -0.183196 | -0.092521 | -0.038368 | -0.079228 | -0.016666 |
| eur | 0.875312 | 1.000000 | 0.012266 | -0.087286 | -0.007164 | -0.106937 | 0.002723 | 0.002332 | 0.003274 | 0.003873 | 0.004907 | 0.092218 | 0.086167 | 0.067377 | 0.030152 | 0.010865 | 0.047035 | 0.006751 | 0.004275 | 0.043296 | 0.045509 | 0.055589 | 0.065463 | 0.074229 | 0.080456 | 0.091720 | 0.088314 | -0.019756 | -0.053026 | 0.004446 | -0.080060 | -0.030046 | 0.015466 | 0.012364 | -0.066220 | -0.012464 | -0.028265 | -0.192577 | -0.129222 | -0.047632 | -0.077128 | -0.042727 |
| imoex | 0.015139 | 0.012266 | 1.000000 | 0.758414 | 0.120188 | 0.003795 | -0.405386 | -0.335829 | -0.280028 | -0.248010 | -0.227072 | -0.267495 | -0.338223 | -0.374288 | -0.398434 | -0.414442 | -0.387764 | -0.416508 | -0.413388 | -0.051914 | -0.058137 | -0.072252 | -0.101350 | -0.131200 | -0.156277 | -0.240112 | -0.199511 | 0.725593 | 0.529361 | 0.640721 | 0.396456 | 0.612800 | 0.671601 | 0.634254 | 0.511772 | 0.625327 | 0.642786 | 0.267929 | 0.346005 | 0.354476 | 0.165174 | 0.377328 |
| irts | -0.108508 | -0.087286 | 0.758414 | 1.000000 | 0.066351 | 0.028698 | -0.356281 | -0.269281 | -0.213181 | -0.185698 | -0.168621 | -0.228202 | -0.305627 | -0.352552 | -0.381319 | -0.387006 | -0.371038 | -0.381508 | -0.370902 | -0.078235 | -0.082933 | -0.085654 | -0.105693 | -0.126022 | -0.143057 | -0.203882 | -0.173003 | 0.594884 | 0.412502 | 0.463247 | 0.303006 | 0.447823 | 0.508090 | 0.481986 | 0.434651 | 0.515798 | 0.484851 | 0.256500 | 0.294266 | 0.303708 | 0.097301 | 0.318972 |
| brent | 0.027208 | -0.007164 | 0.120188 | 0.066351 | 1.000000 | 0.114795 | -0.006373 | 0.011361 | 0.024453 | 0.029775 | 0.032784 | 0.019238 | 0.010998 | 0.011006 | 0.005366 | -0.003620 | 0.010416 | -0.005926 | -0.006901 | 0.050445 | 0.050097 | 0.049589 | 0.047304 | 0.044123 | 0.040704 | 0.024387 | 0.033056 | 0.055399 | 0.073130 | 0.212715 | 0.084413 | 0.155285 | 0.109829 | 0.130786 | 0.049205 | 0.034462 | 0.065457 | -0.025201 | -0.005669 | -0.003968 | -0.000626 | -0.006934 |
| gold | -0.080204 | -0.106937 | 0.003795 | 0.028698 | 0.114795 | 1.000000 | 0.036322 | 0.034173 | 0.025275 | 0.017233 | 0.011259 | 0.003899 | -0.007424 | -0.013698 | 0.003455 | 0.024067 | -0.007572 | 0.030247 | 0.034131 | -0.037382 | -0.037074 | -0.038249 | -0.034756 | -0.029327 | -0.023534 | -0.000306 | -0.011545 | -0.025651 | 0.036593 | 0.040070 | 0.395681 | 0.060496 | 0.048678 | -0.045848 | -0.058068 | -0.040064 | 0.047933 | 0.003960 | -0.006923 | -0.046822 | 0.011410 | -0.052837 |
| 10Y | -0.033898 | 0.002723 | -0.405386 | -0.356281 | -0.006373 | 0.036322 | 1.000000 | 0.893678 | 0.763223 | 0.679991 | 0.625297 | 0.328075 | 0.446853 | 0.575765 | 0.773806 | 0.919430 | 0.681132 | 0.965512 | 0.991936 | 0.196527 | 0.202715 | 0.205972 | 0.229157 | 0.249767 | 0.264750 | 0.306705 | 0.286424 | -0.325319 | -0.240139 | -0.322649 | -0.106415 | -0.243958 | -0.266892 | -0.293437 | -0.204101 | -0.238702 | -0.305085 | -0.466265 | -0.599811 | -0.677565 | -0.243387 | -0.650078 |
| 15Y | -0.036629 | 0.002332 | -0.335829 | -0.269281 | 0.011361 | 0.034173 | 0.893678 | 1.000000 | 0.968153 | 0.923243 | 0.885136 | 0.248729 | 0.272518 | 0.321455 | 0.478471 | 0.669600 | 0.391898 | 0.758213 | 0.833886 | 0.116490 | 0.122779 | 0.123512 | 0.148790 | 0.173081 | 0.191961 | 0.239716 | 0.220194 | -0.270430 | -0.185527 | -0.282777 | -0.082928 | -0.192782 | -0.214612 | -0.213253 | -0.188363 | -0.205426 | -0.257894 | -0.309016 | -0.463046 | -0.563824 | -0.198231 | -0.494701 |
| 20Y | -0.031181 | 0.003274 | -0.280028 | -0.213181 | 0.024453 | 0.025275 | 0.763223 | 0.968153 | 1.000000 | 0.988659 | 0.969281 | 0.200857 | 0.182371 | 0.193660 | 0.314184 | 0.500700 | 0.241403 | 0.597316 | 0.686240 | 0.079238 | 0.085177 | 0.085711 | 0.110249 | 0.134331 | 0.153312 | 0.198330 | 0.181569 | -0.226124 | -0.145692 | -0.248294 | -0.063118 | -0.157296 | -0.173247 | -0.161705 | -0.170097 | -0.175480 | -0.219105 | -0.221612 | -0.372863 | -0.472163 | -0.165928 | -0.389641 |
| 25Y | -0.025370 | 0.003873 | -0.248010 | -0.185698 | 0.029775 | 0.017233 | 0.679991 | 0.923243 | 0.988659 | 1.000000 | 0.994994 | 0.171692 | 0.135593 | 0.130679 | 0.232395 | 0.410644 | 0.167613 | 0.507117 | 0.598395 | 0.059396 | 0.064895 | 0.065496 | 0.088615 | 0.111571 | 0.129810 | 0.171988 | 0.156978 | -0.200940 | -0.124159 | -0.230812 | -0.051995 | -0.140178 | -0.149811 | -0.140252 | -0.159299 | -0.160222 | -0.200064 | -0.172988 | -0.321749 | -0.417971 | -0.145824 | -0.332512 |
| 30Y | -0.020474 | 0.004907 | -0.227072 | -0.168621 | 0.032784 | 0.011259 | 0.625297 | 0.885136 | 0.969281 | 0.994994 | 1.000000 | 0.151996 | 0.109081 | 0.096018 | 0.186362 | 0.357307 | 0.126720 | 0.451952 | 0.542776 | 0.043914 | 0.049017 | 0.049487 | 0.071336 | 0.093357 | 0.111085 | 0.152856 | 0.137906 | -0.185160 | -0.110870 | -0.218618 | -0.044469 | -0.129354 | -0.134133 | -0.128361 | -0.152248 | -0.149992 | -0.188151 | -0.144054 | -0.290102 | -0.384296 | -0.133209 | -0.297780 |
| 1Y | 0.076908 | 0.092218 | -0.267495 | -0.228202 | 0.019238 | 0.003899 | 0.328075 | 0.248729 | 0.200857 | 0.171692 | 0.151996 | 1.000000 | 0.897374 | 0.726594 | 0.492230 | 0.401629 | 0.579286 | 0.372605 | 0.348792 | 0.334023 | 0.362612 | 0.435726 | 0.561078 | 0.680120 | 0.771636 | 0.984055 | 0.904798 | -0.229726 | -0.167669 | -0.153407 | -0.034053 | -0.122820 | -0.129482 | -0.127047 | -0.090130 | -0.084328 | -0.086338 | -0.650496 | -0.552670 | -0.478428 | -0.565763 | -0.551358 |
| 2Y | 0.078563 | 0.086167 | -0.338223 | -0.305627 | 0.010998 | -0.007424 | 0.446853 | 0.272518 | 0.182371 | 0.135593 | 0.109081 | 0.897374 | 1.000000 | 0.932126 | 0.725810 | 0.596552 | 0.814208 | 0.542555 | 0.492981 | 0.257665 | 0.277511 | 0.331010 | 0.422841 | 0.514855 | 0.590499 | 0.829149 | 0.717013 | -0.279621 | -0.218957 | -0.212022 | -0.088454 | -0.183316 | -0.186818 | -0.189696 | -0.143154 | -0.134401 | -0.158831 | -0.724591 | -0.647547 | -0.589336 | -0.556648 | -0.658513 |
| 3Y | 0.057461 | 0.067377 | -0.374288 | -0.352552 | 0.011006 | -0.013698 | 0.575765 | 0.321455 | 0.193660 | 0.130679 | 0.096018 | 0.726594 | 0.932126 | 1.000000 | 0.908867 | 0.778509 | 0.965491 | 0.710406 | 0.642261 | 0.308337 | 0.321529 | 0.358886 | 0.416891 | 0.472412 | 0.516526 | 0.663705 | 0.589649 | -0.304081 | -0.234907 | -0.254332 | -0.112523 | -0.216591 | -0.224294 | -0.238542 | -0.156124 | -0.167501 | -0.203670 | -0.742864 | -0.684714 | -0.648372 | -0.476832 | -0.710792 |
| 5Y | 0.010729 | 0.030152 | -0.398434 | -0.381319 | 0.005366 | 0.003455 | 0.773806 | 0.478471 | 0.314184 | 0.232395 | 0.186362 | 0.492230 | 0.725810 | 0.908867 | 1.000000 | 0.951805 | 0.982974 | 0.901839 | 0.840813 | 0.318296 | 0.325713 | 0.343914 | 0.371288 | 0.393280 | 0.407687 | 0.452531 | 0.427055 | -0.319969 | -0.243471 | -0.291633 | -0.112527 | -0.241582 | -0.256100 | -0.286860 | -0.170580 | -0.202754 | -0.258167 | -0.669642 | -0.681412 | -0.680583 | -0.336552 | -0.720971 |
| 7Y | -0.016453 | 0.010865 | -0.414442 | -0.387006 | -0.003620 | 0.024067 | 0.919430 | 0.669600 | 0.500700 | 0.410644 | 0.357307 | 0.401629 | 0.596552 | 0.778509 | 0.951805 | 1.000000 | 0.886786 | 0.989678 | 0.961156 | 0.270914 | 0.277178 | 0.286471 | 0.308634 | 0.326316 | 0.337720 | 0.371127 | 0.352403 | -0.331783 | -0.252152 | -0.315262 | -0.108563 | -0.254425 | -0.272167 | -0.307637 | -0.191726 | -0.227955 | -0.294038 | -0.588010 | -0.666527 | -0.698947 | -0.278777 | -0.712430 |
| 4Y | 0.031959 | 0.047035 | -0.387764 | -0.371038 | 0.010416 | -0.007572 | 0.681132 | 0.391898 | 0.241403 | 0.167613 | 0.126720 | 0.579286 | 0.814208 | 0.965491 | 0.982974 | 0.886786 | 1.000000 | 0.822688 | 0.752847 | 0.328514 | 0.337834 | 0.363789 | 0.400960 | 0.433229 | 0.456455 | 0.531306 | 0.491388 | -0.312959 | -0.237621 | -0.276435 | -0.115394 | -0.231357 | -0.242904 | -0.267618 | -0.161104 | -0.187340 | -0.233265 | -0.710108 | -0.684049 | -0.666799 | -0.392158 | -0.719023 |
| 8Y | -0.024352 | 0.006751 | -0.416508 | -0.381508 | -0.005926 | 0.030247 | 0.965512 | 0.758213 | 0.597316 | 0.507117 | 0.451952 | 0.372605 | 0.542555 | 0.710406 | 0.901839 | 0.989678 | 0.822688 | 1.000000 | 0.990622 | 0.245759 | 0.251908 | 0.258596 | 0.280606 | 0.298750 | 0.310907 | 0.345605 | 0.327130 | -0.333631 | -0.251586 | -0.321946 | -0.108473 | -0.254844 | -0.274443 | -0.308738 | -0.198623 | -0.235598 | -0.303618 | -0.547321 | -0.649570 | -0.699148 | -0.263787 | -0.698251 |
| 9Y | -0.030006 | 0.004275 | -0.413388 | -0.370902 | -0.006901 | 0.034131 | 0.991936 | 0.833886 | 0.686240 | 0.598395 | 0.542776 | 0.348792 | 0.492981 | 0.642261 | 0.840813 | 0.961156 | 0.752847 | 0.990622 | 1.000000 | 0.220602 | 0.226746 | 0.231467 | 0.253935 | 0.273198 | 0.286684 | 0.324737 | 0.305500 | -0.331447 | -0.247480 | -0.324366 | -0.108087 | -0.251076 | -0.272578 | -0.303745 | -0.202703 | -0.239147 | -0.307127 | -0.506486 | -0.626775 | -0.691897 | -0.252856 | -0.677129 |
| 1W | 0.043817 | 0.043296 | -0.051914 | -0.078235 | 0.050445 | -0.037382 | 0.196527 | 0.116490 | 0.079238 | 0.059396 | 0.043914 | 0.334023 | 0.257665 | 0.308337 | 0.318296 | 0.270914 | 0.328514 | 0.245759 | 0.220602 | 1.000000 | 0.999263 | 0.983929 | 0.946658 | 0.881207 | 0.805629 | 0.442194 | 0.633379 | -0.029665 | -0.033376 | -0.018658 | -0.006592 | 0.025553 | -0.017216 | 0.006496 | 0.090767 | 0.007795 | 0.031702 | -0.321206 | -0.183085 | -0.177017 | -0.153761 | -0.199885 |
| 2W | 0.044945 | 0.045509 | -0.058137 | -0.082933 | 0.050097 | -0.037074 | 0.202715 | 0.122779 | 0.085177 | 0.064895 | 0.049017 | 0.362612 | 0.277511 | 0.321529 | 0.325713 | 0.277178 | 0.337834 | 0.251908 | 0.226746 | 0.999263 | 1.000000 | 0.987735 | 0.956805 | 0.897349 | 0.826311 | 0.471645 | 0.660270 | -0.035936 | -0.036784 | -0.022297 | -0.005952 | 0.022140 | -0.019707 | 0.003818 | 0.088552 | 0.006667 | 0.030620 | -0.336910 | -0.195984 | -0.187086 | -0.167891 | -0.212364 |
| 1M | 0.054935 | 0.055589 | -0.072252 | -0.085654 | 0.049589 | -0.038249 | 0.205972 | 0.123512 | 0.085711 | 0.065496 | 0.049487 | 0.435726 | 0.331010 | 0.358886 | 0.343914 | 0.286471 | 0.363789 | 0.258596 | 0.231467 | 0.983929 | 0.987735 | 1.000000 | 0.985349 | 0.941082 | 0.881357 | 0.547146 | 0.730279 | -0.049420 | -0.047373 | -0.032919 | -0.007036 | 0.015019 | -0.027516 | -0.005842 | 0.081358 | 0.007663 | 0.025024 | -0.373454 | -0.229189 | -0.215097 | -0.219015 | -0.244733 |
| 2M | 0.060463 | 0.065463 | -0.101350 | -0.105693 | 0.047304 | -0.034756 | 0.229157 | 0.148790 | 0.110249 | 0.088615 | 0.071336 | 0.561078 | 0.422841 | 0.416891 | 0.371288 | 0.308634 | 0.400960 | 0.280606 | 0.253935 | 0.946658 | 0.956805 | 0.985349 | 1.000000 | 0.984857 | 0.948435 | 0.670334 | 0.832644 | -0.078508 | -0.064190 | -0.050404 | -0.005453 | -0.001992 | -0.039978 | -0.020345 | 0.066010 | 0.000684 | 0.016538 | -0.437640 | -0.286627 | -0.259939 | -0.285356 | -0.299770 |
| 3M | 0.065105 | 0.074229 | -0.131200 | -0.126022 | 0.044123 | -0.029327 | 0.249767 | 0.173081 | 0.134331 | 0.111571 | 0.093357 | 0.680120 | 0.514855 | 0.472412 | 0.393280 | 0.326316 | 0.433229 | 0.298750 | 0.273198 | 0.881207 | 0.897349 | 0.941082 | 0.984857 | 1.000000 | 0.988933 | 0.781420 | 0.914481 | -0.108100 | -0.081665 | -0.068341 | -0.004771 | -0.020588 | -0.053279 | -0.036365 | 0.046246 | -0.008606 | 0.005353 | -0.495114 | -0.342322 | -0.303201 | -0.350117 | -0.352612 |
| 4M | 0.068235 | 0.080456 | -0.156277 | -0.143057 | 0.040704 | -0.023534 | 0.264750 | 0.191961 | 0.153312 | 0.129810 | 0.111085 | 0.771636 | 0.590499 | 0.516526 | 0.407687 | 0.337720 | 0.456455 | 0.310907 | 0.286684 | 0.805629 | 0.826311 | 0.881357 | 0.948435 | 0.988933 | 1.000000 | 0.861559 | 0.963924 | -0.132602 | -0.096515 | -0.083408 | -0.005131 | -0.037162 | -0.065000 | -0.050741 | 0.026515 | -0.018151 | -0.006053 | -0.536761 | -0.386544 | -0.337535 | -0.401597 | -0.394176 |
| 9M | 0.074613 | 0.091720 | -0.240112 | -0.203882 | 0.024387 | -0.000306 | 0.306705 | 0.239716 | 0.198330 | 0.171988 | 0.152856 | 0.984055 | 0.829149 | 0.663705 | 0.452531 | 0.371127 | 0.531306 | 0.345605 | 0.324737 | 0.442194 | 0.471645 | 0.547146 | 0.670334 | 0.781420 | 0.861559 | 1.000000 | 0.963713 | -0.208416 | -0.148527 | -0.134735 | -0.019827 | -0.099972 | -0.110957 | -0.106203 | -0.059820 | -0.064586 | -0.061383 | -0.629074 | -0.515411 | -0.442454 | -0.540719 | -0.514459 |
| 6M | 0.072082 | 0.088314 | -0.199511 | -0.173003 | 0.033056 | -0.011545 | 0.286424 | 0.220194 | 0.181569 | 0.156978 | 0.137906 | 0.904798 | 0.717013 | 0.589649 | 0.427055 | 0.352403 | 0.491388 | 0.327130 | 0.305500 | 0.633379 | 0.660270 | 0.730279 | 0.832644 | 0.914481 | 0.963924 | 0.963713 | 1.000000 | -0.173360 | -0.122629 | -0.109458 | -0.008952 | -0.068022 | -0.086987 | -0.077756 | -0.014436 | -0.038977 | -0.030755 | -0.593227 | -0.456756 | -0.392815 | -0.481813 | -0.459516 |
| SBER | 0.003562 | -0.019756 | 0.725593 | 0.594884 | 0.055399 | -0.025651 | -0.325319 | -0.270430 | -0.226124 | -0.200940 | -0.185160 | -0.229726 | -0.279621 | -0.304081 | -0.319969 | -0.331783 | -0.312959 | -0.333631 | -0.331447 | -0.029665 | -0.035936 | -0.049420 | -0.078508 | -0.108100 | -0.132602 | -0.208416 | -0.173360 | 1.000000 | 0.500870 | 0.515621 | 0.286410 | 0.411321 | 0.480338 | 0.531465 | 0.457813 | 0.483290 | 0.499996 | 0.218063 | 0.275861 | 0.272626 | 0.114148 | 0.292168 |
| YDEX | -0.051122 | -0.053026 | 0.529361 | 0.412502 | 0.073130 | 0.036593 | -0.240139 | -0.185527 | -0.145692 | -0.124159 | -0.110870 | -0.167669 | -0.218957 | -0.234907 | -0.243471 | -0.252152 | -0.237621 | -0.251586 | -0.247480 | -0.033376 | -0.036784 | -0.047373 | -0.064190 | -0.081665 | -0.096515 | -0.148527 | -0.122629 | 0.500870 | 1.000000 | 0.481552 | 0.293704 | 0.432775 | 0.378703 | 0.427605 | 0.441680 | 0.426664 | 0.441788 | 0.155740 | 0.199289 | 0.222347 | 0.128568 | 0.222083 |
| ROSN | 0.025896 | 0.004446 | 0.640721 | 0.463247 | 0.212715 | 0.040070 | -0.322649 | -0.282777 | -0.248294 | -0.230812 | -0.218618 | -0.153407 | -0.212022 | -0.254332 | -0.291633 | -0.315262 | -0.276435 | -0.321946 | -0.324366 | -0.018658 | -0.022297 | -0.032919 | -0.050404 | -0.068341 | -0.083408 | -0.134735 | -0.109458 | 0.515621 | 0.481552 | 1.000000 | 0.373122 | 0.546017 | 0.498800 | 0.602713 | 0.450157 | 0.541364 | 0.546645 | 0.196833 | 0.288684 | 0.286321 | 0.149086 | 0.292040 |
| PLZL | -0.062618 | -0.080060 | 0.396456 | 0.303006 | 0.084413 | 0.395681 | -0.106415 | -0.082928 | -0.063118 | -0.051995 | -0.044469 | -0.034053 | -0.088454 | -0.112523 | -0.112527 | -0.108563 | -0.115394 | -0.108473 | -0.108087 | -0.006592 | -0.005952 | -0.007036 | -0.005453 | -0.004771 | -0.005131 | -0.019827 | -0.008952 | 0.286410 | 0.293704 | 0.373122 | 1.000000 | 0.344844 | 0.299340 | 0.302796 | 0.285055 | 0.316729 | 0.390941 | 0.054479 | 0.049789 | 0.091369 | 0.061609 | 0.097538 |
| LKOH | 0.010914 | -0.030046 | 0.612800 | 0.447823 | 0.155285 | 0.060496 | -0.243958 | -0.192782 | -0.157296 | -0.140178 | -0.129354 | -0.122820 | -0.183316 | -0.216591 | -0.241582 | -0.254425 | -0.231357 | -0.254844 | -0.251076 | 0.025553 | 0.022140 | 0.015019 | -0.001992 | -0.020588 | -0.037162 | -0.099972 | -0.068022 | 0.411321 | 0.432775 | 0.546017 | 0.344844 | 1.000000 | 0.423207 | 0.493209 | 0.410819 | 0.449992 | 0.447929 | 0.167565 | 0.175957 | 0.185654 | 0.109873 | 0.234738 |
| GAZP | 0.008987 | 0.015466 | 0.671601 | 0.508090 | 0.109829 | 0.048678 | -0.266892 | -0.214612 | -0.173247 | -0.149811 | -0.134133 | -0.129482 | -0.186818 | -0.224294 | -0.256100 | -0.272167 | -0.242904 | -0.274443 | -0.272578 | -0.017216 | -0.019707 | -0.027516 | -0.039978 | -0.053279 | -0.065000 | -0.110957 | -0.086987 | 0.480338 | 0.378703 | 0.498800 | 0.299340 | 0.423207 | 1.000000 | 0.588156 | 0.380452 | 0.464221 | 0.586244 | 0.149123 | 0.206562 | 0.196125 | 0.083128 | 0.228487 |
| NVTK | 0.034316 | 0.012364 | 0.634254 | 0.481986 | 0.130786 | -0.045848 | -0.293437 | -0.213253 | -0.161705 | -0.140252 | -0.128361 | -0.127047 | -0.189696 | -0.238542 | -0.286860 | -0.307637 | -0.267618 | -0.308738 | -0.303745 | 0.006496 | 0.003818 | -0.005842 | -0.020345 | -0.036365 | -0.050741 | -0.106203 | -0.077756 | 0.531465 | 0.427605 | 0.602713 | 0.302796 | 0.493209 | 0.588156 | 1.000000 | 0.462015 | 0.524801 | 0.541780 | 0.172517 | 0.207932 | 0.240812 | 0.071473 | 0.259211 |
| MOEX | -0.100134 | -0.066220 | 0.511772 | 0.434651 | 0.049205 | -0.058068 | -0.204101 | -0.188363 | -0.170097 | -0.159299 | -0.152248 | -0.090130 | -0.143154 | -0.156124 | -0.170580 | -0.191726 | -0.161104 | -0.198623 | -0.202703 | 0.090767 | 0.088552 | 0.081358 | 0.066010 | 0.046246 | 0.026515 | -0.059820 | -0.014436 | 0.457813 | 0.441680 | 0.450157 | 0.285055 | 0.410819 | 0.380452 | 0.462015 | 1.000000 | 0.550543 | 0.469871 | 0.114634 | 0.185807 | 0.229998 | 0.095199 | 0.201620 |
| CHMF | -0.046182 | -0.012464 | 0.625327 | 0.515798 | 0.034462 | -0.040064 | -0.238702 | -0.205426 | -0.175480 | -0.160222 | -0.149992 | -0.084328 | -0.134401 | -0.167501 | -0.202754 | -0.227955 | -0.187340 | -0.235598 | -0.239147 | 0.007795 | 0.006667 | 0.007663 | 0.000684 | -0.008606 | -0.018151 | -0.064586 | -0.038977 | 0.483290 | 0.426664 | 0.541364 | 0.316729 | 0.449992 | 0.464221 | 0.524801 | 0.550543 | 1.000000 | 0.555604 | 0.076133 | 0.161315 | 0.193919 | 0.078076 | 0.200171 |
| GMKN | -0.030118 | -0.028265 | 0.642786 | 0.484851 | 0.065457 | 0.047933 | -0.305085 | -0.257894 | -0.219105 | -0.200064 | -0.188151 | -0.086338 | -0.158831 | -0.203670 | -0.258167 | -0.294038 | -0.233265 | -0.303618 | -0.307127 | 0.031702 | 0.030620 | 0.025024 | 0.016538 | 0.005353 | -0.006053 | -0.061383 | -0.030755 | 0.499996 | 0.441788 | 0.546645 | 0.390941 | 0.447929 | 0.586244 | 0.541780 | 0.469871 | 0.555604 | 1.000000 | 0.123795 | 0.204888 | 0.213209 | 0.082043 | 0.240373 |
| RU000A0JS3W6 | -0.183196 | -0.192577 | 0.267929 | 0.256500 | -0.025201 | 0.003960 | -0.466265 | -0.309016 | -0.221612 | -0.172988 | -0.144054 | -0.650496 | -0.724591 | -0.742864 | -0.669642 | -0.588010 | -0.710108 | -0.547321 | -0.506486 | -0.321206 | -0.336910 | -0.373454 | -0.437640 | -0.495114 | -0.536761 | -0.629074 | -0.593227 | 0.218063 | 0.155740 | 0.196833 | 0.054479 | 0.167565 | 0.149123 | 0.172517 | 0.114634 | 0.076133 | 0.123795 | 1.000000 | 0.720708 | 0.634908 | 0.522044 | 0.666136 |
| RU000A0ZYUA9 | -0.092521 | -0.129222 | 0.346005 | 0.294266 | -0.005669 | -0.006923 | -0.599811 | -0.463046 | -0.372863 | -0.321749 | -0.290102 | -0.552670 | -0.647547 | -0.684714 | -0.681412 | -0.666527 | -0.684049 | -0.649570 | -0.626775 | -0.183085 | -0.195984 | -0.229189 | -0.286627 | -0.342322 | -0.386544 | -0.515411 | -0.456756 | 0.275861 | 0.199289 | 0.288684 | 0.049789 | 0.175957 | 0.206562 | 0.207932 | 0.185807 | 0.161315 | 0.204888 | 0.720708 | 1.000000 | 0.725987 | 0.476328 | 0.745951 |
| RU000A100EF5 | -0.038368 | -0.047632 | 0.354476 | 0.303708 | -0.003968 | -0.046822 | -0.677565 | -0.563824 | -0.472163 | -0.417971 | -0.384296 | -0.478428 | -0.589336 | -0.648372 | -0.680583 | -0.698947 | -0.666799 | -0.699148 | -0.691897 | -0.177017 | -0.187086 | -0.215097 | -0.259939 | -0.303201 | -0.337535 | -0.442454 | -0.392815 | 0.272626 | 0.222347 | 0.286321 | 0.091369 | 0.185654 | 0.196125 | 0.240812 | 0.229998 | 0.193919 | 0.213209 | 0.634908 | 0.725987 | 1.000000 | 0.435348 | 0.795631 |
| RU000A101QE0 | -0.079228 | -0.077128 | 0.165174 | 0.097301 | -0.000626 | 0.011410 | -0.243387 | -0.198231 | -0.165928 | -0.145824 | -0.133209 | -0.565763 | -0.556648 | -0.476832 | -0.336552 | -0.278777 | -0.392158 | -0.263787 | -0.252856 | -0.153761 | -0.167891 | -0.219015 | -0.285356 | -0.350117 | -0.401597 | -0.540719 | -0.481813 | 0.114148 | 0.128568 | 0.149086 | 0.061609 | 0.109873 | 0.083128 | 0.071473 | 0.095199 | 0.078076 | 0.082043 | 0.522044 | 0.476328 | 0.435348 | 1.000000 | 0.401694 |
| RU000A1028E3 | -0.016666 | -0.042727 | 0.377328 | 0.318972 | -0.006934 | -0.052837 | -0.650078 | -0.494701 | -0.389641 | -0.332512 | -0.297780 | -0.551358 | -0.658513 | -0.710792 | -0.720971 | -0.712430 | -0.719023 | -0.698251 | -0.677129 | -0.199885 | -0.212364 | -0.244733 | -0.299770 | -0.352612 | -0.394176 | -0.514459 | -0.459516 | 0.292168 | 0.222083 | 0.292040 | 0.097538 | 0.234738 | 0.228487 | 0.259211 | 0.201620 | 0.200171 | 0.240373 | 0.666136 | 0.745951 | 0.795631 | 0.401694 | 1.000000 |
Упростим матрицу и оставим средние изменения по классам активов
from matplotlib.colors import LinearSegmentedColormap
# Считаем датафрейм с изменениями в долях (не в %)
# для процентных ставок смотрим просто разность, для остальных pct_change
simplified_all_data_diff = pd.concat([
df_index.pct_change() * 100,
rates.diff().mean(axis=1).rename('rates'),
stocks.pct_change(fill_method=None).mean(axis=1).rename('stocks') * 100,
bond_prices.pct_change().mean(axis=1).rename('bonds') * 100,
], axis=1).dropna() / 100
simplified_all_data_diff = simplified_all_data_diff[
simplified_all_data_diff.index.isin(all_data_diff.index)
]
corr_matrix = simplified_all_data_diff.corr()
fig, ax = plt.subplots(figsize=(6, 6))
sns.heatmap(
corr_matrix,
annot=True,
fmt='.2f',
square=True,
cbar=False,
cmap=LinearSegmentedColormap.from_list('rg',["r", "w", "g"], N=256),
vmin=-1,
vmax=1
)
ax.tick_params(rotation=0)
ax.set_title('Корреляция риск факторов')
plt.show()
# Нормализуем данные
# from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
# scaler = StandardScaler()
# scaled_data = scaler.fit_transform(simplified_all_data_diff)
# Применяем PCA
pca = PCA(n_components=4)
pca_result = pca.fit_transform(simplified_all_data_diff)
risk_factors = [
'Market',
'Oil & Market',
'Currency',
'Gold'
]
pca_components = pd.DataFrame(
pca.components_.T,
columns=risk_factors,
index=simplified_all_data_diff.columns
)
print("Объясненная дисперсия:", pca.explained_variance_ratio_)
print('В сумме:', pca.explained_variance_ratio_.sum())
print('Веса компонент в новых координатах')
display(pca_components)
fig, ax = plt.subplots(dpi=120)
sns.heatmap(
pca_components.T,
annot=True,
fmt='.2f',
square=True,
cbar=False,
cmap='Greens',
)
ax.tick_params(rotation=0)
ax.set_title('Веса компонент в новых координатах')
plt.show()
Объясненная дисперсия: [0.40182144 0.31052946 0.114242 0.0762775 ] В сумме: 0.9028703947592925 Веса компонент в новых координатах
| Market | Oil & Market | Currency | Gold | |
|---|---|---|---|---|
| usd | -0.022758 | -0.030425 | -0.683685 | 0.202367 |
| eur | -0.026432 | -0.020728 | -0.665495 | 0.159560 |
| imoex | 0.473680 | 0.252210 | -0.094766 | -0.004090 |
| irts | 0.540283 | 0.340082 | 0.055135 | -0.103402 |
| brent | 0.487694 | -0.868746 | -0.000868 | -0.078944 |
| gold | 0.046462 | -0.055822 | 0.271684 | 0.948657 |
| rates | -0.018094 | -0.017541 | 0.000200 | 0.000897 |
| stocks | 0.488884 | 0.243427 | -0.062077 | 0.127395 |
| bonds | 0.056264 | 0.044494 | 0.001833 | -0.021591 |
Полученные риск-факторы¶
Market- рыночный риск (40% дисперсии) - движение индекса и стоимости акцийOil & Market- товарно-рыночный риск (31% дисперсии) - движение индекса и стоимости нефтиCurrency- валютный риск (11.4% дисперсии) - изменение курса валютGold- изменение цен на золото (7.6% дисперсии) - изменение курса золота
Важно, что для 2 и 4 пунктов ключевые составляющие риска взяты с минусом, поэтому их интерпретация может быть не совсем очевидной
dynamic_df = simplified_all_data_diff.copy() # изменения везде в долях
dynamic_df.iloc[0, :] = 1
# Делаем доходность в формате 1+r
dynamic_df.iloc[1:, :] += 1
# Считаем финальную стоимость как 1 * П(1+r_i) и переводим в %
dynamic_df = (dynamic_df.cumprod() - 1) * 100
dynamic_df.head()
| usd | eur | imoex | irts | brent | gold | rates | stocks | bonds | |
|---|---|---|---|---|---|---|---|---|---|
| Дата | |||||||||
| 2023-01-02 | 0.0 | 0.0 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 2023-01-03 | 0.0 | 0.0 | 0.861605 | -0.774778 | -4.434874 | 0.866375 | -0.092275 | 0.696268 | 0.396547 |
| 2023-01-04 | 0.0 | 0.0 | 0.663844 | -2.131671 | -9.393551 | 1.666950 | -0.020215 | 0.710829 | 0.594524 |
| 2023-01-05 | 0.0 | 0.0 | 0.118378 | -3.065114 | -8.404144 | 0.520922 | 0.041168 | 0.067248 | 0.691732 |
| 2023-01-06 | 0.0 | 0.0 | 0.105379 | -2.871420 | -8.543825 | 2.304120 | 0.030157 | 0.159182 | 0.595732 |
plot_ts_plotly(
dynamic_df.reset_index(),
x='Дата',
y=dynamic_df.columns,
title='Динамика риск-факторов до PCA',
xaxis_title='Дата',
yaxis_title='% от стоимости на начало периода'
)
pca_diff = simplified_all_data_diff @ pca_components
pca_dynamic_df = pca_diff.copy()
pca_dynamic_df.iloc[0, :] = 1
# Делаем доходность в формате 1+r
pca_dynamic_df.iloc[1:, :] += 1
pca_dynamic_df = (pca_dynamic_df.cumprod() - 1) * 100
pca_dynamic_df.head()
| Market | Oil & Market | Currency | Gold | |
|---|---|---|---|---|
| Дата | ||||
| 2023-01-02 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 2023-01-03 | -1.768709 | 3.946984 | 0.072347 | 1.328645 |
| 2023-01-04 | -5.018706 | 8.063239 | 0.235266 | 2.648372 |
| 2023-01-05 | -5.588489 | 6.443270 | -0.033943 | 1.480058 |
| 2023-01-06 | -5.447242 | 6.567223 | 0.454347 | 3.193102 |
plot_ts_plotly(
pca_dynamic_df.reset_index(),
x='Дата',
y=pca_dynamic_df.columns,
title='Динамика риск-факторов после PCA',
xaxis_title='Дата',
yaxis_title='% от стоимости на начало периода'
)
for col, color in zip(pca_diff.columns, colors):
plot_decomposed_ts(
pca_dynamic_df[col],
color=color,
)
plt.show()
print('\n\n\n')
res = []
for rf in risk_factors:
print(f'Расширенный тест Дики-Фуллера для {make_str_bold(rf)}')
result = adfuller(pca_diff[rf], autolag='AIC')
print(f'ADF Statistic: {result[0]}')
print(f'p-value: {result[1]}')
if result[1] > result[4]['5%']:
print(f'Ряд {make_str_bold(rf)} стационарен')
res.append(True)
else:
res.append(False)
print()
print()
if all(res):
print(make_str_bold('Все ряды стационарны'))
else:
raise ValueError('Не все ряды стационарны')
Расширенный тест Дики-Фуллера для Market ADF Statistic: -19.865647359046456 p-value: 0.0 Ряд Market стационарен Расширенный тест Дики-Фуллера для Oil & Market ADF Statistic: -21.62788386568742 p-value: 0.0 Ряд Oil & Market стационарен Расширенный тест Дики-Фуллера для Currency ADF Statistic: -19.4280194439589 p-value: 0.0 Ряд Currency стационарен Расширенный тест Дики-Фуллера для Gold ADF Statistic: -23.316949284984386 p-value: 0.0 Ряд Gold стационарен Все ряды стационарны
for rf in risk_factors:
fig, axes = plt.subplots(figsize=(25, 6), dpi=150, ncols=2)
plot_acf(pca_diff[rf].values, ax=axes[0], lags=30)
plot_pacf(pca_diff[rf].values, ax=axes[1], lags=30)
for ax in axes:
make_ax_better(ax)
ax.set_xticks([i for i in range(0, 31, 1)])
ax.set_title(
to_bold(rf) + ' ' + ax.get_title(),
fontsize=25
)
# Функция для быстрого расчёта и отрисовки ACF(ε_t^2) для одного столбца
def plot_acf_squared(series, factor_name, lags=20, ax=None):
# series: pd.Series с ежедневными изменениями (только торговые дни)
eps2 = series.values**2
acf_vals = acf(eps2, nlags=lags, fft=False)
if ax is None:
fig, ax = plt.subplots(figsize=(7, 3))
ax.stem(range(lags+1), acf_vals, basefmt=" ")
ax.set_title(f"ACF(ε²) для фактора {to_bold(factor_name)}", fontsize=16)
ax.set_xlabel("Лаг")
ax.set_ylabel("ACF(ε²)")
ax.axhline(0, color='black', linewidth=0.8)
# ±1.96/√N границы «случайных» автокорреляций
conf = 1.96/np.sqrt(len(eps2))
ax.axhline(conf, color='red', linestyle='--', linewidth=0.7)
ax.axhline(-conf, color='red', linestyle='--', linewidth=0.7)
return acf_vals
for col in pca_diff.columns:
fig, ax = plt.subplots(figsize=(10, 3), dpi=100)
acf_vals = plot_acf_squared(pca_diff[col], col, lags=20, ax=ax)
make_ax_better(ax)
ax.set_xticks([i for i in range(1, 21, 1)])
print(f"Первые 5 ACF² для «{col}»: {np.round(acf_vals[:6], 3)}\n")
plt.show()
Первые 5 ACF² для «Market»: [ 1. 0.055 -0.023 0.06 0.061 0.051]
Первые 5 ACF² для «Oil & Market»: [ 1. 0.045 0. 0.035 -0.036 0.051]
Первые 5 ACF² для «Currency»: [ 1. 0.331 0.158 0.028 -0.013 0.069]
Первые 5 ACF² для «Gold»: [1. 0.02 0.036 0.052 0.12 0.022]
data = pca_diff[
(pca_diff.index.weekday < 5)
]
fig, axes = plt.subplots(figsize=(25, 12), ncols=3, nrows=2)
data_share_out_sigmas = []
for ax, col, color in zip(axes.flatten(), data.columns, colors*20):
sns.histplot(
data[col],
color=color,
ax=ax,
bins=50,
edgecolor='none',
stat='percent'
)
ax.set_title(
col,
fontsize=20,
weight='bold'
)
ax.set_xlabel('')
ax.set_ylabel('% дней')
make_ax_better(ax)
mean = data[col].mean()
std = data[col].std()
left, right = mean - std * 3, mean + std * 3
data_share_3 = data[
(data[col] > right)
| (data[col] < left)
].shape[0] / data.shape[0] * 100
left, right = mean - std * 2, mean + std * 2
data_share_2 = data[
(data[col] > right)
| (data[col] < left)
].shape[0] / data.shape[0] * 100
data_share_out_sigmas.append((col, data_share_2, data_share_3))
for ax in axes.flatten():
if not ax.has_data():
ax.set_xticks([])
ax.set_yticks([])
for sp in ax.spines:
ax.spines[sp].set_visible(False)
plt.tight_layout(pad=2)
fig.suptitle('Распределение изменений для риск-факторов', fontsize=30, weight='bold', y=1.05)
plt.show()
display(
data.describe(percentiles=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.975, 0.99]).round(3).T
)
pd.DataFrame(
data_share_out_sigmas,
columns=[
'Риск-фактор',
rf'Доля данных за $\pm 2 \sigma$',
rf'Доля данных за $\pm 3 \sigma$',
]
).set_index('Риск-фактор').round(3)
| count | mean | std | min | 1% | 2.5% | 5% | 10% | 25% | 50% | 75% | 90% | 95% | 97.5% | 99% | max | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Market | 522.0 | 0.001 | 0.020 | -0.081 | -0.049 | -0.042 | -0.033 | -0.024 | -0.010 | 0.002 | 0.012 | 0.021 | 0.029 | 0.037 | 0.049 | 0.144 |
| Oil & Market | 522.0 | 0.000 | 0.017 | -0.054 | -0.039 | -0.032 | -0.027 | -0.021 | -0.012 | 0.000 | 0.011 | 0.023 | 0.031 | 0.037 | 0.040 | 0.080 |
| Currency | 522.0 | -0.001 | 0.011 | -0.046 | -0.035 | -0.022 | -0.017 | -0.011 | -0.005 | -0.001 | 0.003 | 0.009 | 0.015 | 0.020 | 0.032 | 0.065 |
| Gold | 522.0 | 0.001 | 0.009 | -0.033 | -0.021 | -0.018 | -0.013 | -0.009 | -0.004 | 0.001 | 0.006 | 0.012 | 0.016 | 0.018 | 0.020 | 0.039 |
| Доля данных за $\pm 2 \sigma$ | Доля данных за $\pm 3 \sigma$ | |
|---|---|---|
| Риск-фактор | ||
| Market | 5.364 | 0.575 |
| Oil & Market | 4.789 | 0.383 |
| Currency | 4.981 | 2.299 |
| Gold | 5.747 | 0.958 |
for col in pca_diff.columns:
data = pca_diff[col].dropna().values
mu_hat = np.mean(data)
sigma_hat = np.std(data, ddof=1)
t_stat, p_val = ttest_1samp(data, popmean=0.0)
print(f"Фактор «{make_str_bold(col)}»:")
print(f' Оценка μ = {mu_hat:.5f}, σ = {sigma_hat:.5f}')
print(f" t-статистика = {t_stat:.3f}, p-value = {p_val:.3f}")
if p_val < 0.05:
print(f" → Дрейф {make_str_bold('статистически значим')} (отличается от 0 на уровне 5%).\n")
else:
print(f" → Дрейф {make_str_bold('не статистически значим')} (на уровне 5%).\n")
Фактор «Market»: Оценка μ = 0.00056, σ = 0.01984 t-статистика = 0.641, p-value = 0.522 → Дрейф не статистически значим (на уровне 5%). Фактор «Oil & Market»: Оценка μ = 0.00032, σ = 0.01744 t-статистика = 0.419, p-value = 0.676 → Дрейф не статистически значим (на уровне 5%). Фактор «Currency»: Оценка μ = -0.00072, σ = 0.01058 t-статистика = -1.555, p-value = 0.121 → Дрейф не статистически значим (на уровне 5%). Фактор «Gold»: Оценка μ = 0.00104, σ = 0.00865 t-статистика = 2.744, p-value = 0.006 → Дрейф статистически значим (отличается от 0 на уровне 5%).
Выбор модели¶
На основании уже проведённых вами шагов:
- ADF показал, что все четыре ряда pca_diff стационарны.
- ACF(pca_diff) почти нулевая, и ACF(ε²) тоже мало отличается от шума.
- Дрейф (μ) оказывается либо маленьким, либо статистически незначимым.
Это говорит о том, что ежедневные изменения факторов можно аппроксимировать как независимые инкременты одного и того же процесса с постоянной дисперсией. В контексте непрерывно-временного моделирования единственный «минимально достаточный» кандидат — это арифметический броуновский процесс (ABM), то есть
$$ dX_t = a\,dt + b\,dW_t, $$
где
- $a$ — константа дрейфа (может оказаться нулём, если μ статистически незначим);
- $b$ — волатильность (корень из дисперсии).
Итого: для всех четырёх факторов мы берём модель
$$ \Delta X_t \;=\; X_{t} - X_{t-1} \;\approx\; a\Delta t + b\,\bigl(W_t - W_{t-1}\bigr), \quad \Delta t = 1\text{ день}, $$
что эквивалентно непрерывному
$$ dX_t \,=\, a\,dt \;+\; b\,dW_t. $$
Почему не CIR и не OU?
- CIR: требует, чтобы $X_t\ge0$ всегда, и в нём дисперсия масштабируется как $\sqrt{X_t}$. Ваши «инкременты» (pca_diff) могут принимать и отрицательные значения; к тому же нет явного признака «положительной» динамики с «границей 0».
- OU (Ornstein–Uhlenbeck): описывает стационарный уровень, который «тянет» к среднему. Но у вас уже нет автокорреляций и нет «возврата к среднему» у самих инкрементов — они выглядят как i.i.d. «шум». Если бы у самого ряда X (а не ΔX) была сильная отрицательная автокорреляция, OU стоял бы на повестке.
Оценка параметров¶
Для ABM (арифметического броуновского процесса) при фиксированном шаге $\Delta t$ (один день) имеем:
$$ \Delta X_t \sim \mathcal{N}\bigl(a\Delta t,\;b^2\Delta t\bigr). $$
Значит, оценки MLE для параметров $a$ и $b$ очень просты:
- $\displaystyle \hat a \;=\; \frac{1}{N\,\Delta t}\sum_{i=1}^N \Delta X_i \;=\; \frac{\overline{\Delta X}}{\Delta t}.$
- $\displaystyle \hat b^2 \;=\; \frac{1}{N\,\Delta t}\sum_{i=1}^N (\Delta X_i - \overline{\Delta X})^2 \;=\; \frac{\mathrm{Var}(\Delta X)}{\Delta t}.$
Так как $\Delta t=1$ (годовую нормировку мы не делаем, просто работаем в «днях»), остаётся
$$ \hat a \;=\; \overline{\Delta X}, \qquad \hat b \;=\; \sqrt{\mathrm{Var}(\Delta X)}. $$
results = []
for col in pca_diff.columns:
data = pca_diff[col].dropna().values
a_hat = np.mean(data)
b_hat = np.std(data)
results.append({'factor': col, "a_hat": a_hat, "b_hat": b_hat})
mle_estimation = pd.DataFrame(results)
results_show = mle_estimation.rename(columns={
'factor': 'Фактор',
'a_hat': 'Дрейф $a$',
'b_hat': 'Волатильность $b$',
})
print(make_str_bold("MLE оценка параметров"))
results_show
MLE оценка параметров
| Фактор | Дрейф $a$ | Волатильность $b$ | |
|---|---|---|---|
| 0 | Market | 0.000557 | 0.019824 |
| 1 | Oil & Market | 0.000320 | 0.017427 |
| 2 | Currency | -0.000720 | 0.010570 |
| 3 | Gold | 0.001038 | 0.008637 |
def simulate_abm(mu, sigma, T, N, x0=0.0, dt=1.0):
"""
Симуляция арифметического броуновского процесса (ABM) dX_t = mu * dt + sigma * dW_t.
Параметры:
- mu (float): дрейф (среднее приращение за единицу времени).
- sigma (float): волатильность (стандартное отклонение приращения за единицу времени).
- T (int): число дискретных шагов (например, количество дней).
- N (int): число траекторий для симуляции.
- X0 (float или массив, shape=(N,)): начальное значение(я) процесса. По умолчанию 0.0.
- dt (float): размер временного шага. По умолчанию 1.0 (один день).
Возвращает:
- X (ndarray, shape=(T+1, N)): симулированные траектории,
где строка 0 — начальные значения X0,
а далее накопленные приращения.
"""
x = np.zeros((T + 1, N))
try:
x[0] = x0
except:
x[0] = np.full(N, x0)
# Генерируем приращения: нормально распределены с mean=mu*dt, std=sigma*sqrt(dt)
increments = np.random.normal(
loc=mu * dt,
scale=sigma * np.sqrt(dt),
size=(T, N)
)
x[1:] = x[0] + np.cumsum(increments, axis=0)
return x
N = 100
for _, row in mle_estimation.iterrows():
fig, ax = plt.subplots(figsize=(20, 7), dpi=130)
factor = row['factor']
real_data = np.cumprod(pca_diff[factor].to_numpy() + 1) - 1
paths = simulate_abm(
mu=row['a_hat'],
sigma=row['b_hat'],
T=real_data.shape[0],
N=N,
x0=0.0,
dt=1.0
)
for i in range(paths.shape[1]):
ax.plot(paths[:, i], color='tab:grey', alpha=0.4)
ax.plot(real_data, color='tab:red', lw=3, label='Реальная динамика')
make_ax_better(ax, locators=['x', 'y'])
ax.set_title(f'Симуляция {N} траекторий риск фактора «{factor}»', fontsize=18, weight='bold')
ax.legend(fontsize=16)
plt.show()
Ниже приведён общий подход к пункту 4: «Для всех инструментов, входящих в портфель, реализовать оценку их справедливой стоимости в зависимости от риск-факторов. Критически обсудить выбор модели. Проверить точность модели». Мы будем опираться на состав портфеля (5 государственных облигаций, 10 акций и валютную позицию) и на уже выделенные четыре фактора: Market (рыночный риск), Oil & Market (товарно-рыночный риск), Currency (валютный риск) и Gold (золото). Из документа очевидно, что основным портфелем являются именно эти инструменты.
Общая логика «ценового» (факторного) моделирования¶
Идея факторной модели для оценки «справедливой стоимости» Мы предполагаем, что ежедневные лог-доходности каждого инструмента (или абсолютные изменения цен, если инструмент примерно стабилен) можно описать в линейном факторном виде:
$$ r_{i,t} \;=\; \alpha_i \;+\; \beta_{i,1}\,F^{\text{Market}}_{t} \;+\; \beta_{i,2}\,F^{\text{Oil\&Market}}_{t} \;+\; \beta_{i,3}\,F^{\text{Currency}}_{t} \;+\; \beta_{i,4}\,F^{\text{Gold}}_{t} \;+\;\varepsilon_{i,t}, $$
где:
- $r_{i,t}$ — либо лог-доходность $i$-го инструмента на день $t$, либо процентное (относительное) изменение цены;
- $F^{\cdot}_t$ — значения риск-факторов (Market, Oil&Market, Currency, Gold) в день $t$, полученные на предыдущих шагах;
- $\alpha_i,\,\beta_{i,j}$ — коэффициенты модели для инструмента $i$;
- $\varepsilon_{i,t}$ — остаток (шум), предполагаемый примерно гауссовским (или близким к гауссовскому).
После оценки коэффициентов $\alpha_i,\,\beta_i$ на исторических данных, «справедливую» цену инструмента $i$ на день $T$ (условно говоря, «модельную») можно получить так:
Предположительно, у нас есть «факторы» $F_t$ на день $T$.
Мы считаем предсказанную доходность $\hat r_{i,T}\;=\;\hat\alpha_i + \sum_{j=1}^4 \hat\beta_{i,j} F^j_{T}$.
Если $P_{i,T-1}$ — это фактическая (рыночная) цена $i$-го инструмента на конец (предыдущего) дня $T-1$, то модельная «справедливая» цена $\hat P_{i,T}$ задаётся:
$$ \hat P_{i,T} \;=\; P_{i,T-1} \times \exp\bigl(\hat r_{i,T}\bigr) \quad\text{(если работаем с лог-доходностью)}, $$
или просто $\hat P_{i,T} = P_{i,T-1}\,\bigl(1 + \hat r_{i,T}\bigr)$, если мы моделируем простой процент (для малоизменчивых инструментов, как облигации) в виде $r_{i,t} = \Delta P_{i,t}/P_{i,t-1}$.
Критическое обоснование выбора
- Мы уже убедились, что сами факторные «инкременты» $F^j_t$ примерно i.i.d. и не имеют автокорреляций (пункты 1–3 и 6 предыдущих разделов).
- Линейная факторная модель (метод главных компонент плюс регрессия) — традиционный и прозрачный метод для объяснения доходностей (см., например, CAPM, Fama–French или APT в более общем виде).
- Учитывая отсутствие условной гетероскедастичности у самих факторов (проверка ACF (ε²) дала, кроме «Currency», нулевые ла́ги), можно считать $\varepsilon_{i,t}$ близким к нормальному шуму с постоянной дисперсией (GARCH/ARCH-модель, формально, для остатков можно не вводить, если автокорреляция несущественна).
- Для государственных облигаций возможно более точным был бы метод «дисконтирования» будущих купонов по модели кривой доходностей (и ее собственным факторам). Однако в рамках курсового проекта и с учётом того, что мы уже свели риск-факторы к четырём «обобщённым» (включая «Market» и «Currency»), применение линейной регрессии «цена → факторы» позволяет быстро и прозрачно разложить историю изменений цен облигаций на те же четыре фактора.
- Важно критически указать в отчёте, что для «длинных» облигаций (5 бумаг с датой погашения после 01.01.2025) типично использовать «мотоды кривой доходностей» (bootstrapping, Nelson–Siegel, Svensson и др.), но мы вместо этого применяем факторный подход «цена → факторы», поскольку цель — интегрировать всё в единую систему.
Примечание. В этом примере мы рассматриваем упрощённый «лайт»-вариант:
- Акции и валюту моделируем через «доходность» (линейная регрессия на факторы).
- Облигации тоже моделируем через простую доходность $\Delta P/P_{t-1}$.
Объяснение ключевых моментов кода¶
Чтение данных и синхронизация
- Мы загружаем исторические цены отдельно для акций, облигаций и валют (спот USD/RUB и EUR/RUB).
- Загружаем исторические значения четырёх факторов (Market, Oil & Market, Currency, Gold).
- Делим проверяем, что индексы дат совпадают; оставляем лишь «пересечение» (common_index), чтобы регрессии строились на одних и тех же датах. .
Расчёт доходностей
- Для акций и валют используем лог-доходности $\ln(P_t/P_{t-1})$, что даёт более симметричное распределение ошибок.
- Для облигаций (их цены меняются не столь резко) используем простую доходность $(P_t - P_{t-1})/P_{t-1}$.
- После этого синхронизируем эти доходности (common_ret_index) с факторным DataFrame, чтобы в регрессии $r_{i,t}$ и $F_t$ имели одинаковую временную базу.
Оценка линейных регрессий для каждого инструмента
В цикле по каждому «инструменту» (каждая акция, каждая облигация, каждый FX):
- Строим матрицу $X_t = [\text{const}, F^{\text{Market}}_t, F^{\text{Oil\&Market}}_t, F^\text{Currency}_t, F^\text{Gold}_t]$.
- Оцениваем OLS $\hat r_{i,t} = \hat\alpha_i + \sum \hat\beta_{i,j} F^j_t$.
- Сохраняем коэффициенты $\hat\alpha_i, \hat\beta_{i,j}$.
- Делаем прогноз (train / test) и считаем RMSE, R², MAPE.
Строим «справедливые» (модельные) цены на test-период
Берём цену на последний день обучающей выборки $P_{i,\,t_0}$.
Генерируем для каждого дня test-периода прогноз доходности $\hat r_{i,t}$.
Строим кумулятивно «модельную траекторию» цен $\hat P_{i,t}$ так:
- Если лог-доходность (для акций и FX), то $\hat P_{i,t} = \hat P_{i,t-1} \, \exp(\hat r_{i,t})$.
- Если простая доходность (для облигаций), то $\hat P_{i,t} = \hat P_{i,t-1} \bigl(1 + \hat r_{i,t}\bigr)$.
Получается «модельная кривая» цен, которую сравниваем с фактической $P_{i,t}$.
Оценка «точности» факторной модели
В таблице
results_dfсобрано по каждому инструменту:- $\hat\alpha_i,\,\hat\beta_{i,j}$.
- RMSE_train, R²_train, MAPE_train.
- RMSE_test, R²_test, MAPE_test.
Критическое обсуждение¶
Плюсы выбранного подхода
- Унифицированная «факторная» система для всех инструментов (акции, облигации, FX).
- Прозрачность: сразу видно, какие факторы и в какой мере влияют на цену.
- Простота реализации и интерпретации (все OLS‐регрессии).
Минусы и ограничения
- Для облигаций: из-за фиксированных купонных выплат линеарная регрессия «доходность → факторы» теряет точность в периоды дисконтов и сильно меняющихся кривых ставок. Здесь логично было бы применить модель «bootstrapping yield‐кривой + дисконтирование CF» (DCF). Однако в рамках интеграции с четырьмя факторными компонентами это усложнение выходит за рамки задачи.
- Для валюты: остатки могут быть нерегулярными (sharp jumps) — иногда стоит добавить GARCH в остатки или даже нелинейные факторы (вероятность геополитического шока).
- Для акций: в эпоху 2021–2025 гг. некоторые сектора (IT, сырьё) имели свои «локальные» риски. Линейная модель на первичные четыре фактора не всегда учитывает «отраслевую эффективность».
- Неучтённая временная динамика: все регрессии «одношаговые» (доходность & фактор в один и тот же день). Если бы нужно было предсказывать «справедливую цену» на будущую дату $\tau+1$, стоило бы использовать лаговые факторы $F_{\tau}, F_{\tau-1}, \dots$ и/или ARIMA/GARCH для самих факторов.
Проверка точности
Если R²_test < 0.4, мы бы сочли, что «факторная регрессия не годится» (особенно для облигаций). На практике обычно стремятся к R²_test > 0.7 для большинства инструментов.
Если остатки $\varepsilon_{i,t}$ показывают структуру ARCH (проверить ACF (ε²) для остатков), то в отчёте стоит сделать ремарку:
«У актива X в остатках линейной регрессии проявляется ARCH(1) эффект (ACF² lag1 ≈0.2). Можно добавить GARCH(1,1)-компонент для точной валидации “справедливой цены”».
Для окончательной «проверки» можно оставить hold-out период (т. е. 2024 год) и протестировать там качество 1–2 сделанных моделей.
Общий вывод¶
Выбор модели для оценки справедливой стоимости активов Для оценки справедливой стоимости инструментов портфеля мы выбрали линейную факторную модель (модель множественной линейной регрессии доходности на четыре выделенных риска-фактора: Market, Oil&Market, Currency, Gold).
Этот выбор обусловлен следующими причинами:
Простота и прозрачность: факторный подход позволяет напрямую интерпретировать вклад каждого риск-фактора в динамику цены инструмента, что удобно для анализа и презентации результатов.
Унифицированность: одна модель охватывает акции, облигации и валютные позиции, обеспечивая единый подход к оценке.
Приемлемое качество: при тестировании линейная модель дала удовлетворительные показатели качества (R² ~ 0.5–0.8 для большинства инструментов), что делает её подходящей для целей нашего проекта.
Мы протестировали альтернативные методы — CatBoost и Ridge-регрессию, — но они показали более низкое качество прогнозов на тестовой выборке, а их применение сильно усложняло бы интерпретацию результатов без существенного выигрыша в точности. Поэтому мы сознательно выбрали «дёшево и сердито»: линейную регрессию с понятной структурой и хорошим балансом между качеством и сложностью реализации.
Для облигаций, разумеется, традиционно применяются более сложные методы (DCF с кривыми доходностей), но мы оставили их за рамками проекта, чтобы сохранить единый подход ко всем активам.
import pandas as pd
import numpy as np
import statsmodels.api as sm
from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_percentage_error
import matplotlib.pyplot as plt
def factor_model_forecasting(prices_stocks, prices_bonds, prices_fx, all_data_diff, pca_diff, horizon_days=60, visualize=False, models_coef=None, Print = True):
"""
Факторное моделирование доходностей и цен.
Parameters
----------
prices_stocks : DataFrame
Исторические цены акций.
prices_bonds : DataFrame
Исторические цены облигаций.
prices_fx : DataFrame
Исторические цены валют.
all_data_diff : DataFrame
Доходности всех инструментов.
pca_diff : DataFrame
Доходности факторов (например, после PCA).
horizon_days : int
Горизонт тестирования в днях.
visualize : bool
Построить ли графики результатов.
models_coef : dict or None
Параметры предобученной модели (если есть). Если None — модель обучится заново.
Print : bool
Выведит ли на печать результат.
Returns
-------
results_df : DataFrame
Таблица с результатами обучения и метриками качества прогноза.
fair_prices : dict
Справедливые цены на тестовом интервале по каждому инструменту.
models_coef : dict
Параметры модели (обученные или переданные пользователем).
Example
-------
results_df, fair_prices, models_coef = factor_model_forecasting(
prices_stocks=stocks_df,
prices_bonds=bonds_df,
prices_fx=fx_df,
all_data_diff=all_returns_df,
pca_diff=pca_returns_df,
horizon_days=60,
visualize=True
)
"""
# Функция обучает факторные модели доходностей (OLS),
# оценивает качество прогнозов, рассчитывает справедливые цены инструментов
# на тестовом интервале и визуализирует результаты.
# Если models_coef передан, повторное обучение не производится.
# ───────────────────────────
# 1. Подготовка данных
# ───────────────────────────
all_cols = (
list(prices_stocks.columns)
+ list(prices_bonds.columns)
+ list(prices_fx.columns)
)
all_returns = all_data_diff.dropna()[all_cols].copy()
all_cols = (
list(pd.MultiIndex.from_product([['stocks'], prices_stocks.columns]))
+ list(pd.MultiIndex.from_product([['bonds'], prices_bonds.columns]))
+ list(pd.MultiIndex.from_product([['fx'], prices_fx.columns]))
)
all_returns.columns = pd.MultiIndex.from_tuples(all_cols)
results_summary = []
# ───────────────────────────
# 2. Обучение моделей OLS
# ───────────────────────────
if models_coef is None:
train_idx = all_returns.index[:-horizon_days]
test_idx = all_returns.index[-horizon_days:]
R_train = all_returns.loc[train_idx]
R_test = all_returns.loc[test_idx]
F_train = pca_diff.loc[train_idx]
F_test = pca_diff.loc[test_idx]
models_coef = {}
for instrument in all_returns.columns:
y_train = R_train[instrument]
X_train = sm.add_constant(F_train)
if y_train.std() < 1e-8:
continue
model = sm.OLS(y_train, X_train).fit()
models_coef[instrument] = model.params
X_test = sm.add_constant(F_test)
y_test = R_test[instrument]
y_pred_train = model.predict(X_train)
y_pred_test = model.predict(X_test)
results_summary.append({
'instrument': instrument,
'alpha': model.params.get('const', np.nan),
'beta_Market': model.params.get('Market', np.nan),
'beta_OilM': model.params.get('Oil & Market', np.nan),
'beta_Currency': model.params.get('Currency', np.nan),
'beta_Gold': model.params.get('Gold', np.nan),
'rmse_train': np.sqrt(mean_squared_error(y_train, y_pred_train)),
'r2_train': r2_score(y_train, y_pred_train),
'mape_train': mean_absolute_percentage_error(y_train, y_pred_train),
'rmse_test': np.sqrt(mean_squared_error(y_test, y_pred_test)),
'r2_test': r2_score(y_test, y_pred_test),
'mape_test': mean_absolute_percentage_error(y_test, y_pred_test)
})
results_df = pd.DataFrame(results_summary)
if Print:
print("=== Сводка по тестовой части (Test): ===")
print(results_df[['instrument','r2_test','rmse_test','mape_test']])
# ───────────────────────────
# 3. Справедливые цены
# ───────────────────────────
fair_prices = {}
for instr, params in models_coef.items():
if instr[0] == 'stocks':
price_series = prices_stocks[instr[1]].loc[test_idx]
prev_price = prices_stocks[instr[1]].loc[train_idx[-1]]
elif instr[0] == 'bonds':
price_series = prices_bonds[instr[1]].loc[test_idx]
prev_price = prices_bonds[instr[1]].loc[train_idx[-1]]
else:
price_series = prices_fx[instr[1]].loc[test_idx]
prev_price = prices_fx[instr[1]].loc[train_idx[-1]]
r_hat = (
params['const'] +
params.get('Market', 0) * F_test.get('Market', 0) +
params.get('Oil & Market', 0) * F_test.get('Oil & Market', 0) +
params.get('Currency', 0) * F_test.get('Currency', 0) +
params.get('Gold', 0) * F_test.get('Gold', 0)
)
sim_prices = []
last_price = prev_price
for ret_pred in r_hat.values:
next_price = last_price * (1 + ret_pred)
sim_prices.append(next_price)
last_price = next_price
fair_prices[instr] = pd.Series(sim_prices, index=test_idx)
# ───────────────────────────
# 4. Визуализация (опционально)
# ───────────────────────────
if visualize:
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(12, 8))
axes = axes.flatten()
example_instruments = results_df['instrument'].tolist()[:4]
for ax, instr in zip(axes, example_instruments):
y_test = R_test[instr]
model = sm.OLS(R_train[instr], sm.add_constant(F_train)).fit()
y_pred = model.predict(sm.add_constant(F_test))
ax.plot(y_test.index, y_test.values, label='Факт (доходность)', color='black')
ax.plot(y_test.index, y_pred, label='Прогноз (доходность)', color='red', alpha=0.7)
ax.set_title(f"{instr}\nR²={results_df.loc[results_df['instrument']==instr, 'r2_test'].values[0]:.3f}")
ax.legend(fontsize='small')
ax.grid(True)
plt.tight_layout()
plt.show()
for instr in list(fair_prices.keys())[:4]: # покажем не все, а только несколько для примера
plt.figure(figsize=(8, 4))
if instr[0] == 'stocks':
actual = prices_stocks[instr[1]].loc[test_idx]
elif instr[0] == 'bonds':
actual = prices_bonds[instr[1]].loc[test_idx]
else:
actual = prices_fx[instr[1]].loc[test_idx]
plt.plot(actual.index, actual.values, label='Фактическая цена', linewidth=1.5)
plt.plot(fair_prices[instr].index, fair_prices[instr].values, label='Модельная цена', linestyle='--')
plt.title(f"{instr}")
plt.xlabel("Дата")
plt.ylabel("Цена")
plt.legend(fontsize='small')
plt.grid(True)
plt.show()
return results_df, fair_prices, models_coef
results_df, fair_prices, _ = factor_model_forecasting(
prices_stocks=stocks,
prices_bonds=bond_prices,
prices_fx=df_index[['eur', 'usd']],
all_data_diff=all_data_diff,
pca_diff=pca_diff,
horizon_days=150,
visualize=False,
)
=== Сводка по тестовой части (Test): ===
instrument r2_test rmse_test mape_test
0 (stocks, SBER) 0.650213 0.011246 5.887229e+10
1 (stocks, YDEX) 0.465472 0.015314 1.019057e+13
2 (stocks, ROSN) 0.569411 0.013180 1.050254e+12
3 (stocks, PLZL) 0.318833 0.016822 1.698081e+12
4 (stocks, LKOH) 0.459122 0.011935 9.109942e+11
5 (stocks, GAZP) 0.506796 0.017083 7.345536e+10
6 (stocks, NVTK) 0.528385 0.016282 2.039176e+12
7 (stocks, MOEX) 0.426744 0.014857 1.080323e+12
8 (stocks, CHMF) 0.557711 0.018444 1.150271e+12
9 (stocks, GMKN) 0.532161 0.016131 9.640928e+11
10 (bonds, RU000A0JS3W6) 0.064889 0.003984 2.646913e+10
11 (bonds, RU000A0ZYUA9) 0.050867 0.006027 3.288681e+10
12 (bonds, RU000A100EF5) 0.093418 0.009207 5.696235e+10
13 (bonds, RU000A101QE0) -0.023326 0.001868 4.543881e+09
14 (bonds, RU000A1028E3) 0.067426 0.007667 7.697446e+10
15 (fx, eur) 0.691542 0.006090 4.892348e+10
16 (fx, usd) 0.707863 0.006042 6.335687e+10
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from tqdm.notebook import tqdm
def simulate_factor_paths(mle_estimation, horizons, N_simulations=10000):
"""
Симуляция траекторий факторов с использованием параметров аппроксимации.
Parameters
----------
mle_estimation : DataFrame
Оценённые параметры факторов (должен содержать колонки 'factor', 'a_hat', 'b_hat').
horizons : list of int
Список горизонтов моделирования (в днях).
N_simulations : int, default=10000
Количество симуляций для каждой траектории.
Returns
-------
factor_paths : dict
Словарь вида {horizon: {factor: ndarray (horizon, N_simulations)}} с траекториями факторов.
Example
-------
factor_paths = simulate_factor_paths(mle_estimation, horizons=[21, 60], N_simulations=5000)
"""
factor_paths = {}
for horizon in horizons:
factor_paths[horizon] = {}
for _, row in mle_estimation.iterrows():
factor = row['factor']
paths = simulate_abm(
mu=row['a_hat'],
sigma=row['b_hat'],
T=horizon,
N=N_simulations,
x0=0.0,
dt=1.0
)
factor_paths[horizon][factor] = paths # shape = (horizon, N)
return factor_paths
def simulate_portfolio_value(
prices_stocks, prices_bonds, prices_fx, models_coef, factor_paths_horizon,
initial_positions, prev_prices, SHOW_PROGRESS = True
):
"""
Симуляция стоимости портфеля с ежедневной ребалансировкой, сохраняющей исходные доли инструментов.
Returns
-------
portfolio_values : ndarray
Массив смоделированных конечных стоимостей портфеля (размер = N_simulations).
SHOW_PROGRESS : bool
Показыать прогресс бар или нет
"""
N_sim = next(iter(factor_paths_horizon.values())).shape[1]
horizon = next(iter(factor_paths_horizon.values())).shape[0]
portfolio_values = np.zeros(N_sim)
# 1. Считаем начальные веса активов относительно портфеля
total_initial_value = (
sum(initial_positions['stocks'].values()) +
sum(initial_positions['bonds'].values()) +
sum(initial_positions['fx'].values())
)
weights = {}
for instr_type in ['stocks', 'bonds', 'fx']:
for ticker, amount in initial_positions[instr_type].items():
weights[(instr_type, ticker)] = amount / total_initial_value
for i in tqdm(range(N_sim), desc="Симуляции портфеля (ежедн. ребалансировка)", disable= not SHOW_PROGRESS):
factor_returns = pd.DataFrame({
factor: factor_paths_horizon[factor][:, i]
for factor in factor_paths_horizon
})
# 2. Генерируем справедливые цены инструментов на каждый день
fair_prices_sim = {}
for instr, params in models_coef.items():
r_hat = (
params['const'] +
params.get('Market', 0) * factor_returns['Market'].values +
params.get('Oil & Market', 0) * factor_returns['Oil & Market'].values +
params.get('Currency', 0) * factor_returns['Currency'].values +
params.get('Gold', 0) * factor_returns['Gold'].values
)
prices = prev_prices[instr[1]] * (1 + np.cumsum(r_hat))
fair_prices_sim[instr] = prices # shape = (horizon, )
# 3. Симуляция ежедневной ребалансировки
portfolio_value = total_initial_value # Начальная стоимость портфеля
for t in range(horizon):
# Определяем цены на начало и конец дня t
if t == 0:
day_start_prices = prev_prices
else:
day_start_prices = pd.Series({
instr[1]: fair_prices_sim[instr][t-1]
for instr in models_coef.keys()
})
day_end_prices = pd.Series({
instr[1]: fair_prices_sim[instr][t]
for instr in models_coef.keys()
})
# Считаем стоимость портфеля на конец текущего дня
day_end_value = 0.0
for instr, w in weights.items():
ticker = instr[1]
asset_value_start_of_day = w * portfolio_value
daily_growth_factor = day_end_prices[ticker] / day_start_prices[ticker]
asset_value_end_of_day = asset_value_start_of_day * daily_growth_factor
day_end_value += asset_value_end_of_day
portfolio_value = day_end_value # обновляем стоимость портфеля для следующего дня
portfolio_values[i] = portfolio_value # сохраняем итоговое значение после всех дней
return portfolio_values
def estimate_risk(portfolio_values, initial_value):
"""
Оценка риск-метрик портфеля: Value-at-Risk (VaR) и Expected Shortfall (ES).
Parameters
----------
portfolio_values : ndarray
Смоделированные конечные стоимости портфеля.
initial_value : float
Исходная стоимость портфеля на дату ребалансировки.
Returns
-------
var_99 : float
Value-at-Risk на уровне 99%.
es_975 : float
Expected Shortfall на уровне 97.5%.
losses : ndarray
Массив относительных убытков для всех симуляций.
Example
-------
var, es, losses = estimate_risk(portfolio_values, initial_value)
"""
losses = 1 - portfolio_values / initial_value
var_99 = np.percentile(losses, 99)
es_975 = losses[losses >= np.percentile(losses, 97.5)].mean()
return var_99, es_975, losses
def run_full_risk_assessment(mle_estimation, models_coef, prices_stocks, prices_bonds, prices_fx, all_data, prev_trade_date, horizons, portfolio_positions_rub, N_simulations=10000):
"""
Полный процесс оценки рисков портфеля с использованием факторных моделей и симуляций.
Parameters
----------
mle_estimation : DataFrame
Оценённые параметры факторов (колонки 'factor', 'a_hat', 'b_hat').
models_coef : dict
Коэффициенты факторных моделей для каждого инструмента.
prices_stocks : DataFrame
Исторические цены акций.
prices_bonds : DataFrame
Исторические цены облигаций.
prices_fx : DataFrame
Исторические цены валют.
all_data : DataFrame
Полные исторические данные по ценам всех инструментов.
prev_trade_date : str or Timestamp
Дата последней ребалансировки портфеля.
horizons : list of int
Список горизонтов тестирования (в днях).
portfolio_positions_rub : dict
Структура портфеля в рублях {'stocks': ..., 'bonds': ..., 'fx': ...}.
N_simulations : int, default=10000
Количество симуляций траекторий факторов.
Returns
-------
results : dict
Результаты симуляций по каждому горизонту, включая VaR, ES и распределение убытков.
Example
-------
results = run_full_risk_assessment(mle_estimation, models_coef, prices_stocks, prices_bonds, prices_fx, all_data, '2024-11-29', [21, 60, 120], portfolio_positions_rub, 10000)
"""
results = {}
# 1. Получаем цены на момент ребалансировки
prev_prices = all_data.loc[prev_trade_date]
# 2. Строим траектории факторов для всех горизонтов
factor_paths = simulate_factor_paths(mle_estimation, horizons, N_simulations)
# 3. Для каждого горизонта рассчитываем стоимость портфеля и метрики риска
for horizon in horizons:
factor_paths_h = factor_paths[horizon]
simulated_portfolio_values = simulate_portfolio_value(
prices_stocks, prices_bonds, prices_fx, models_coef,
factor_paths_h, portfolio_positions_rub, prev_prices
)
initial_value = (
sum(portfolio_positions_rub['stocks'].values()) +
sum(portfolio_positions_rub['bonds'].values()) +
sum(portfolio_positions_rub['fx'].values())
)
var, es, losses_pct = estimate_risk(simulated_portfolio_values, initial_value)
results[horizon] = {
'VaR_99': var,
'ES_97.5': es,
'Losses': losses_pct
}
# Визуализация распределения потерь
plt.figure(figsize=(8, 4))
plt.hist(losses_pct, bins=100, density=True, alpha=0.6, color='steelblue')
plt.axvline(var, color='red', linestyle='--', label=f'VaR 99% = {var:.4f}')
plt.axvline(es, color='darkred', linestyle='-', label=f'ES 97.5% = {es:.4f}')
plt.title(f"Распределение потерь портфеля\nГоризонт = {horizon} дней")
plt.xlabel("Относительный убыток")
plt.ylabel("Плотность")
plt.legend()
plt.grid(True)
plt.show()
return results
portfolio_positions_rub = {
'stocks': {
'SBER': 1e6, 'YDEX': 1e6, 'ROSN': 1e6, 'PLZL': 1e6, 'LKOH': 1e6,
'GAZP': 1e6, 'NVTK': 1e6, 'MOEX': 1e6, 'CHMF': 1e6, 'GMKN': 1e6
},
'bonds': {
'RU000A0JS3W6': 10e6, 'RU000A0ZYUA9': 10e6, 'RU000A100EF5': 10e6,
'RU000A101QE0': 10e6, 'RU000A1028E3': 10e6
},
'fx': {
'eur': 100e6, 'usd': 100e6
}
}
results_df, fair_prices, models_coef = factor_model_forecasting(
prices_stocks=stocks,
prices_bonds=bond_prices,
prices_fx=df_index[['eur', 'usd']],
all_data_diff=all_data_diff,
pca_diff=pca_diff,
horizon_days=32,
visualize=False,
)
results = run_full_risk_assessment(
mle_estimation=mle_estimation, # DataFrame с оценками параметров факторов (столбцы: factor, a_hat, b_hat)
models_coef=models_coef, # dict с коэффициентами моделей факторного прогнозирования
prices_stocks=stocks, # DataFrame цен акций (дата — индекс)
prices_bonds=bond_prices, # DataFrame цен облигаций
prices_fx=df_index[['usd', 'eur']], # DataFrame цен валют
all_data=all_data, # Series всех цен (index — дата, index внутри Series — тикеры)
prev_trade_date='2024-11-29', # Дата последней ребалансировки (строка или pd.Timestamp)
horizons=[1, 10], # Горизонты прогнозирования в днях
portfolio_positions_rub=portfolio_positions_rub, # dict с составом портфеля (в рублях)
N_simulations=10000 # Количество симуляций
)
=== Сводка по тестовой части (Test): ===
instrument r2_test rmse_test mape_test
0 (stocks, SBER) 0.822481 0.012486 6.812187e+10
1 (stocks, YDEX) 0.671684 0.017636 2.912977e+11
2 (stocks, ROSN) 0.605747 0.016121 1.719478e+11
3 (stocks, PLZL) -0.163572 0.024680 8.492239e+11
4 (stocks, LKOH) 0.515639 0.014992 2.482964e+11
5 (stocks, GAZP) 0.649138 0.017023 2.255625e+10
6 (stocks, NVTK) 0.655669 0.017897 3.905681e+12
7 (stocks, MOEX) 0.410297 0.018857 1.074522e+11
8 (stocks, CHMF) 0.702588 0.020273 8.411381e+09
9 (stocks, GMKN) 0.647529 0.018499 4.568842e+11
10 (bonds, RU000A0JS3W6) 0.038857 0.006871 7.214206e+10
11 (bonds, RU000A0ZYUA9) 0.100474 0.008563 9.873122e+10
12 (bonds, RU000A100EF5) 0.260956 0.011341 1.546307e+11
13 (bonds, RU000A101QE0) -0.062383 0.002583 5.065225e+09
14 (bonds, RU000A1028E3) 0.214291 0.009877 1.324369e+11
15 (fx, eur) 0.538182 0.009459 3.974787e+09
16 (fx, usd) 0.486855 0.010068 3.976232e+10
Симуляции портфеля (ежедн. ребалансировка): 0%| | 0/10000 [00:00<?, ?it/s]
Симуляции портфеля (ежедн. ребалансировка): 0%| | 0/10000 [00:00<?, ?it/s]
from scipy.stats import chi2
def perform_var_backtesting(
all_data,
prices_stocks,
prices_bonds,
prices_fx,
all_data_diff,
pca_diff,
mle_estimation,
portfolio_positions_rub,
backtest_year=2024,
n_simulations=2500,
horizont_simulation = 10
):
"""
Проводит ежедневный бэктестинг VaR модели на исторических данных.
Parameters
----------
all_data : pd.DataFrame
Полный датафрейм с ценами всех активов. Индекс - дата.
prices_stocks : pd.DataFrame
Датафрейм с ценами только акций.
prices_bonds : pd.DataFrame
Датафрейм с ценами только облигаций.
prices_fx : pd.DataFrame
Датафрейм с ценами только валют.
all_data_diff : pd.DataFrame
Датафрейм с дневными доходностями (или изменениями для ставок).
pca_diff : pd.DataFrame
Датафрейм с доходностями PCA-факторов.
mle_estimation : pd.DataFrame
Датафрейм с оцененными параметрами дрейфа и волатильности для факторов.
portfolio_positions_rub : dict
Словарь, описывающий состав портфеля в рублях.
backtest_year : int, optional
Год, за который проводится бэктестинг (по умолчанию 2024).
n_simulations : int, optional
Количество симуляций для оценки риска на каждый день (по умолчанию 2500).
horizont_simulation: int, optional
Горизонт симуляции (по умолчанию 10 дней)
Returns
-------
backtest_results_df : pd.DataFrame
Датафрейм с результатами бэктестинга. Индекс - дата, колонки -
VaR, ES, фактический убыток (Loss) и индикатор пробоя (Breach)
для общего портфеля и каждого из подпортфелей.
Example
-------
>>> backtesting_df = perform_var_backtesting(
... all_data=all_data,
... prices_stocks=stocks,
... prices_bonds=bond_prices,
... prices_fx=df_index[['eur', 'usd']],
... all_data_diff=all_data_diff,
... pca_diff=pca_diff,
... mle_estimation=mle_estimation,
... portfolio_positions_rub=portfolio_positions_rub,
... backtest_year=2024
... horizont_simulation=10
... )
"""
# Определяем торговые дни в указанном году для итерации
trading_days = all_data.loc[str(backtest_year)].index
backtest_results = []
# Создаем конфигурации для портфеля и каждого подпортфеля
portfolio_configs = {
"total": portfolio_positions_rub,
"stocks": {'stocks': portfolio_positions_rub.get('stocks', {}), 'bonds': {}, 'fx': {}},
"bonds": {'stocks': {}, 'bonds': portfolio_positions_rub.get('bonds', {}), 'fx': {}},
"fx": {'stocks': {}, 'bonds': {}, 'fx': portfolio_positions_rub.get('fx', {})}
}
# Итерируемся по дням, начиная со второго дня года
for i in tqdm(range(1, len(trading_days)), desc=f"Бэктестинг VaR за {backtest_year} год"):
current_date = trading_days[i]
prev_trade_date = trading_days[i-1]
# --- 1. Обучение модели на расширяющемся окне ---
train_history_slice = all_data.loc[:prev_trade_date]
_, _, models_coef = factor_model_forecasting(
prices_stocks = train_history_slice[prices_stocks.columns],
prices_bonds = train_history_slice[prices_bonds.columns],
prices_fx = train_history_slice[prices_fx.columns],
all_data_diff = all_data_diff.loc[:prev_trade_date],
pca_diff = pca_diff.loc[:prev_trade_date],
horizon_days = horizont_simulation,
visualize = False,
Print = False
)
if not models_coef:
print(f"Не удалось обучить модель для даты {prev_trade_date}, пропуск шага.")
continue
# --- 2. Симуляция и расчет риск-метрик ---
factor_paths = simulate_factor_paths(mle_estimation, horizons=[1], N_simulations=n_simulations)
factor_paths_h1 = factor_paths[1]
previous_day_prices = all_data.loc[prev_trade_date]
daily_results = {'date': current_date}
# Рассчитываем риск для каждого портфеля/подпортфеля
for p_name, p_config in portfolio_configs.items():
initial_value = sum(val for asset_class in p_config.values() for val in asset_class.values())
if initial_value == 0:
# Если подпортфель пуст, его риски и убытки равны нулю
daily_results[f'VaR_{p_name}'] = 0
daily_results[f'ES_{p_name}'] = 0
daily_results[f'Loss_{p_name}'] = 0
daily_results[f'Breach_{p_name}'] = 0
continue
# Симулируем стоимость портфеля
sim_values = simulate_portfolio_value(None, None, None, models_coef, factor_paths_h1, p_config, previous_day_prices, SHOW_PROGRESS=False)
# Оцениваем риски с помощью обновленной функции
var_99, es, _ = estimate_risk(sim_values, initial_value)
# --- 3. Расчет фактического убытка ---
current_day_prices = all_data.loc[current_date]
# Стоимость портфеля на конец текущего дня
current_day_value = 0
for asset_type, assets in p_config.items():
for ticker, rub_value in assets.items():
# Предполагаем, что состав портфеля в рублях фиксирован на prev_trade_date
units = rub_value / previous_day_prices.get(ticker, 1) # get() для безопасности
current_day_value += units * current_day_prices.get(ticker, 1)
actual_return = (current_day_value - initial_value) / initial_value
actual_loss = -actual_return
# --- 4. Сохранение результатов ---
daily_results[f'VaR_{p_name}'] = var_99
daily_results[f'ES_{p_name}'] = es
daily_results[f'Loss_{p_name}'] = actual_loss
daily_results[f'Breach_{p_name}'] = 1 if actual_loss > var_99 else 0
backtest_results.append(daily_results)
return pd.DataFrame(backtest_results).set_index('date')
# Определим состав портфеля в рублях (как в вашем коде)
portfolio_positions_rub = {
'stocks': {
'SBER': 1e6, 'YDEX': 1e6, 'ROSN': 1e6, 'PLZL': 1e6, 'LKOH': 1e6,
'GAZP': 1e6, 'NVTK': 1e6, 'MOEX': 1e6, 'CHMF': 1e6, 'GMKN': 1e6
},
'bonds': {
'RU000A0JS3W6': 10e6, 'RU000A0ZYUA9': 10e6, 'RU000A100EF5': 10e6,
'RU000A101QE0': 10e6, 'RU000A1028E3': 10e6
},
'fx': {
'eur': 100e6, 'usd': 100e6
}
}
# Запускаем бэктестинг на 2024 год
# ПРИМЕЧАНИЕ: Код может выполняться долго (от 30 минут до нескольких часов)
backtesting_df = perform_var_backtesting(
all_data=all_data,
prices_stocks=stocks,
prices_bonds=bond_prices,
prices_fx=df_index[['eur', 'usd']],
all_data_diff=all_data_diff,
pca_diff=pca_diff,
mle_estimation=mle_estimation,
portfolio_positions_rub=portfolio_positions_rub,
backtest_year=2024,
n_simulations=2000,
horizont_simulation = 10
)
Бэктестинг VaR за 2024 год: 0%| | 0/261 [00:00<?, ?it/s]
Тест Купиеца (Kupiec, 1995) – Тест безусловного покрытия (Unconditional Coverage - UC)¶
Описание: Тест Купиеца, также известный как тест "Proportion of Failures" (POF), проверяет, соответствует ли наблюдаемая доля нарушений VaR (Value-at-Risk) теоретической вероятности нарушений, заданной уровнем доверия VaR. Он оценивает, является ли количество фактических превышений VaR статистически совместимым с тем количеством, которое ожидается при правильной модели. Основное предположение теста Купиеца состоит в том, что каждое нарушение VaR является независимым событием, которое следует распределению Бернулли.
Гипотезы:
- $H_0$: Наблюдаемая доля нарушений равна ожидаемой вероятности нарушений ($p$).
- $H_1$: Наблюдаемая доля нарушений не равна ожидаемой вероятности нарушений ($p$).
Формула: Статистика теста Купиеца представляет собой отношение правдоподобия (Likelihood Ratio - LR), которое асимптотически распределено как $\chi^2$ с 1 степенью свободы:
$$LR_{UC} = -2 \ln \left( \frac{(1-p)^{N-x} p^x}{(1 - x/N)^{N-x} (x/N)^x} \right)$$
Где:
- $N$ – общее количество наблюдений (например, количество дней).
- $x$ – количество наблюдаемых нарушений VaR (число случаев, когда фактический убыток превысил VaR).
- $p$ – ожидаемая вероятность нарушения VaR (например, для 99% VaR, $p = 1 - 0.99 = 0.01$).
# Тест Kupiec'а
def kupiec_pof_test(T, N, p_star=0.01, confidence_level=0.95):
"""
Проводит тест Купика на долю отказов (POF-test).
Parameters
----------
T : int
Общее количество наблюдений.
N : int
Количество пробоев VaR.
p_star : float, default=0.01
Ожидаемая доля пробоев (1% для 99% VaR).
confidence_level : float, default=0.95
Уровень доверия для определения критического значения.
Returns
-------
lr_statistic : float
Значение LR-статистики.
p_value : float
P-значение теста.
result : str
Вывод о принятии или отвержении гипотезы H0.
"""
if N == 0 or N == T:
return np.nan, np.nan, "Невозможно рассчитать (0 или 100% пробоев)"
p_hat = N / T
# Формула статистики отношения правдоподобия
log_likelihood_h1 = (T - N) * np.log(1 - p_hat) + N * np.log(p_hat)
log_likelihood_h0 = (T - N) * np.log(1 - p_star) + N * np.log(p_star)
lr_statistic = -2 * (log_likelihood_h0 - log_likelihood_h1)
# p-value рассчитывается из хи-квадрат распределения с 1 степенью свободы
p_value = 1 - chi2.cdf(lr_statistic, 1)
critical_value = chi2.ppf(confidence_level, 1)
if lr_statistic > critical_value:
result = f"Отвергнуть H0 (статистика {lr_statistic:.2f} > {critical_value:.2f})"
else:
result = f"Не отвергать H0 (статистика {lr_statistic:.2f} <= {critical_value:.2f})"
return lr_statistic, p_value, result
print("Результаты бэктестинга (первые 5 дней 2024 г.):")
display(backtesting_df.head())
# Проверка гипотезы
T = len(backtesting_df)
expected_breaches = T * 0.01
summary_data = []
portfolio_map = {
'total': 'Весь портфель',
'stocks': 'Акции',
'bonds': 'Облигации',
'fx': 'Валюта'
}
for p_key, p_name in portfolio_map.items():
N = backtesting_df[f'Breach_{p_key}'].sum()
lr_stat, p_val, conclusion = kupiec_pof_test(T, N, p_star=0.01)
summary_data.append({
'Портфель': p_name,
'Торговых дней (T)': T,
'Ожидалось пробоев (1%)': f"{expected_breaches:.2f}",
'Факт. пробоев (N)': N,
'Доля пробоев': f"{(N/T)*100:.2f}%",
'LR-статистика': f"{lr_stat:.3f}" if not np.isnan(lr_stat) else "n/a",
'p-value': f"{p_val:.3f}" if not np.isnan(p_val) else "n/a",
'Вывод (alpha=5%)': conclusion
})
summary_df = pd.DataFrame(summary_data)
print("\nИтоговая таблица валидации VaR 99% за 2024 год:")
display(summary_df)
Результаты бэктестинга (первые 5 дней 2024 г.):
| VaR_total | ES_total | Loss_total | Breach_total | VaR_stocks | ES_stocks | Loss_stocks | Breach_stocks | VaR_bonds | ES_bonds | Loss_bonds | Breach_bonds | VaR_fx | ES_fx | Loss_fx | Breach_fx | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| date | ||||||||||||||||
| 2024-01-02 | 0.012316 | 0.012454 | -0.000000 | 0 | 0.024205 | 0.024166 | -0.000000 | 0 | 0.002309 | 0.002279 | -0.000000 | 0 | 0.016204 | 0.016377 | -0.0 | 0 |
| 2024-01-03 | 0.013584 | 0.013310 | -0.000942 | 0 | 0.025869 | 0.025959 | -0.015581 | 0 | 0.002455 | 0.002439 | -0.001785 | 0 | 0.017852 | 0.017401 | -0.0 | 0 |
| 2024-01-04 | 0.012671 | 0.012659 | 0.000212 | 0 | 0.026149 | 0.025497 | 0.003479 | 0 | 0.002218 | 0.002229 | 0.000404 | 0 | 0.016850 | 0.016492 | -0.0 | 0 |
| 2024-01-05 | 0.011801 | 0.012146 | 0.000040 | 0 | 0.023600 | 0.024727 | -0.001788 | 0 | 0.002143 | 0.002119 | 0.000566 | 0 | 0.015683 | 0.015939 | -0.0 | 0 |
| 2024-01-08 | 0.012482 | 0.012633 | -0.000377 | 0 | 0.024507 | 0.024542 | -0.006999 | 0 | 0.001968 | 0.001996 | -0.000559 | 0 | 0.016353 | 0.016535 | -0.0 | 0 |
Итоговая таблица валидации VaR 99% за 2024 год:
| Портфель | Торговых дней (T) | Ожидалось пробоев (1%) | Факт. пробоев (N) | Доля пробоев | LR-статистика | p-value | Вывод (alpha=5%) | |
|---|---|---|---|---|---|---|---|---|
| 0 | Весь портфель | 261 | 2.61 | 6 | 2.30% | 3.254 | 0.071 | Не отвергать H0 (статистика 3.25 <= 3.84) |
| 1 | Акции | 261 | 2.61 | 12 | 4.60% | 18.179 | 0.000 | Отвергнуть H0 (статистика 18.18 > 3.84) |
| 2 | Облигации | 261 | 2.61 | 70 | 26.82% | 345.041 | 0.000 | Отвергнуть H0 (статистика 345.04 > 3.84) |
| 3 | Валюта | 261 | 2.61 | 7 | 2.68% | 5.107 | 0.024 | Отвергнуть H0 (статистика 5.11 > 3.84) |
Тест Кристофферсена (Christoffersen, 1998) – Тест условного покрытия (Conditional Coverage - CC)¶
Описание: Тест Кристофферсена расширяет тест Купиеца, добавляя проверку независимости нарушений VaR. Он проверяет не только правильную частоту нарушений (безусловное покрытие), но и то, являются ли эти нарушения независимыми друг от друга во времени. Это важно, поскольку модель VaR может показывать правильное общее количество нарушений, но если эти нарушения имеют тенденцию группироваться (кластеризоваться), это указывает на то, что модель неадекватно улавливает динамику риска. Тест Кристофферсена разбивается на две части: тест безусловного покрытия (который идентичен тесту Купиеца) и тест независимости.
Гипотезы: Тест Кристофферсена проверяет объединенную гипотезу:
- $H_0$: Модель VaR обладает как правильным безусловным покрытием, так и независимыми нарушениями.
- $H_1$: Модель VaR не удовлетворяет одному или обоим этим условиям.
Статистика теста Кристофферсена также является отношением правдоподобия и имеет асимптотическое распределение $\chi^2$ с 2 степенями свободы:
$$LR_{CC} = LR_{UC} + LR_{IND}$$
Где $LR_{UC}$ — это статистика теста Купиеца, а $LR_{IND}$ — статистика теста независимости.
Формула для теста независимости ($LR_{IND}$): Для расчета $LR_{IND}$ необходимо определить переходные вероятности Марковской цепи первого порядка для последовательности нарушений. Определим $I_t$ как индикаторную переменную, равную 1, если произошло нарушение VaR в момент $t$, и 0 в противном случае.
Определим следующие частоты:
- $N_{00}$: Количество дней, когда не было нарушения, за которым не последовало нарушение.
- $N_{01}$: Количество дней, когда не было нарушения, за которым последовало нарушение.
- $N_{10}$: Количество дней, когда было нарушение, за которым не последовало нарушение.
- $N_{11}$: Количество дней, когда было нарушение, за которым последовало нарушение.
Оценочные вероятности перехода:
- $\pi_{01} = \frac{N_{01}}{N_{00} + N_{01}}$ (вероятность нарушения, если не было нарушения в предыдущий день)
- $\pi_{11} = \frac{N_{11}}{N_{10} + N_{11}}$ (вероятность нарушения, если было нарушение в предыдущий день)
Также определим безусловную вероятность нарушения:
- $\pi = \frac{N_{01} + N_{11}}{N_{00} + N_{01} + N_{10} + N_{11}}$ (что эквивалентно $x/N$ в тесте Купиеца).
Статистика $LR_{IND}$ рассчитывается как:
$$LR_{IND} = -2 \ln \left( \frac{(1-\pi)^{N_{00}+N_{10}} \pi^{N_{01}+N_{11}}}{(1-\pi_{01})^{N_{00}} \pi_{01}^{N_{01}} (1-\pi_{11})^{N_{10}} \pi_{11}^{N_{11}}} \right)$$
Эта статистика асимптотически распределена как $\chi^2$ с 1 степенью свободы.
Тест Хурлина и Топкави (Hurlin & Topkavi, 2007) – Двойной условный тест (Double Conditional Test - DCC)¶
Описание: Тест Хурлина и Топкави (2007) является расширением теста Кристофферсена и предлагает подход к тестированию условного покрытия, который учитывает не только зависимость от предыдущего нарушения, но и потенциальную зависимость от значения предыдущего убытка. Хотя их работа фокусируется на "двойном условном тесте", который учитывает как предыдущее состояние (нарушение/не нарушение), так и размер убытка, для целей простого описания с формулами, мы можем рассмотреть его как дальнейшее усложнение идеи условного покрытия.
К сожалению, явная и общепринятая формула "теста Хурлина и Топкави (2007)" как единой, компактной LR-статистики, аналогичной Купиецу или Кристофферсену, не так широко цитируется. Их работа скорее предлагает более общий фреймворк для тестирования динамических свойств моделей VaR, выходящий за рамки простой бинарной последовательности нарушений. Они обсуждают "динамический тест точности" (Dynamic Accuracy Test) и "динамический тест независимости" (Dynamic Independence Test), которые могут включать дополнительные информационные переменные.
В общем, их подход направлен на то, чтобы проверить гипотезы вида $P(I_t = 1 | \mathcal{F}_{t-1}) = p$, где $\mathcal{F}_{t-1}$ - это информационный набор, доступный в $t-1$, который может включать не только $I_{t-1}$, но и другие переменные, такие как предыдущие значения VaR, доходности или убытки.
Концептуально, основные идеи, лежащие в основе подхода Hurlin & Topkavi (2007), включают:
- Расширение информационного набора: Вместо того чтобы полагаться только на бинарную последовательность нарушений ($I_t$), они предлагают включать в условную вероятность другие переменные, которые могут влиять на будущие нарушения (например, предыдущие доходности, волатильность, размер предыдущего убытка).
- Динамические модели для вероятности нарушения: Вероятность нарушения $p_t = P(I_t = 1 | \mathcal{F}_{t-1})$ не обязательно является константой $p$, а может быть динамической и зависеть от $\mathcal{F}_{t-1}$.
- Тестирование специфических форм зависимости: Их тесты могут быть направлены на выявление конкретных типов зависимости, которые не улавливаются простыми марковскими цепями первого порядка.
Хотя прямая "формула" для всего их теста в виде одного LR-статистика отсутствует, общая идея заключается в использовании моделей, таких как логит или пробит, для моделирования вероятности нарушения $I_t$ как функции от предыдущих значений и затем тестирования коэффициентов этих моделей.
Например, если бы мы хотели смоделировать $P(I_t=1|\mathcal{F}_{t-1})$ с учетом предыдущего значения $I_{t-1}$ и $r_{t-1}$ (предыдущей доходности), мы могли бы использовать логит-модель:
$$P(I_t=1|\mathcal{F}_{t-1}) = \frac{1}{1 + e^{-(\beta_0 + \beta_1 I_{t-1} + \beta_2 r_{t-1})}}$$
Тогда тест на независимость и условное покрытие будет заключаться в проверке гипотез о коэффициентах ($\beta_1$, $\beta_2$ и т.д.).
В целом, тест Hurlin & Topkavi (2007) не предоставляет единой универсальной формулы LR-статистики, как Kupiec или Christoffersen, а скорее набор методологий для более продвинутого анализа условного покрытия, включающего в себя более богатый информационный набор и динамические модели.
import numpy as np
import pandas as pd
from scipy.stats import chi2
from scipy.optimize import minimize
# --- Тест Кристофферсена ---
def christoffersen_test(breaches: pd.Series, p_star: float = 0.01):
"""
Проводит тест Кристофферсена на условное покрытие (частота + независимость).
Parameters
----------
breaches : pd.Series
Временной ряд из 0 (нет пробоя) и 1 (есть пробой).
p_star : float, default=0.01
Ожидаемая доля пробоев.
Returns
-------
dict
Словарь со статистиками, p-value и выводами для каждого компонента теста.
"""
n = len(breaches)
n1 = breaches.sum()
n0 = n - n1
if n1 < 2:
return {
'LR_uc': np.nan, 'p_val_uc': np.nan, 'result_uc': 'Слишком мало пробоев',
'LR_ind': np.nan, 'p_val_ind': np.nan, 'result_ind': 'Слишком мало пробоев',
'LR_cc': np.nan, 'p_val_cc': np.nan, 'result_cc': 'Слишком мало пробоев',
}
# 1. Тест на безусловное покрытие (Купик)
lr_uc, p_val_uc, res_uc = kupiec_pof_test(n, n1, p_star)
# 2. Тест на независимость
n01, n11, n00, n10 = 0, 0, 0, 0
for i in range(1, n):
if breaches.iloc[i-1] == 0 and breaches.iloc[i] == 1: n01 += 1
if breaches.iloc[i-1] == 1 and breaches.iloc[i] == 1: n11 += 1
if breaches.iloc[i-1] == 0 and breaches.iloc[i] == 0: n00 += 1
if breaches.iloc[i-1] == 1 and breaches.iloc[i] == 0: n10 += 1
pi0 = (n01) / (n00 + n01) if (n00 + n01) > 0 else 0
pi1 = (n11) / (n10 + n11) if (n10 + n11) > 0 else 0
pi = (n01 + n11) / n
if pi < 1e-9 or pi0 < 1e-9 or pi1 < 1e-9: #
lr_ind = np.nan
else:
log_L0 = (n00 + n10) * np.log(1 - pi) + (n01 + n11) * np.log(pi)
log_L1 = n00 * np.log(1 - pi0) + n01 * np.log(pi0) + n10 * np.log(1 - pi1) + n11 * np.log(pi1)
lr_ind = -2 * (log_L0 - log_L1)
p_val_ind = 1 - chi2.cdf(lr_ind, 1)
res_ind = "Отвергнуть H0 (Кластеризация)" if p_val_ind < 0.05 else "Не отвергать H0"
# 3. Совмещенный тест
lr_cc = lr_uc + lr_ind
p_val_cc = 1 - chi2.cdf(lr_cc, 2)
res_cc = "Отвергнуть H0" if p_val_cc < 0.05 else "Не отвергать H0"
return {
'LR_uc': lr_uc, 'p_val_uc': p_val_uc, 'result_uc': res_uc.split(' ')[0],
'LR_ind': lr_ind, 'p_val_ind': p_val_ind, 'result_ind': res_ind,
'LR_cc': lr_cc, 'p_val_cc': p_val_cc, 'result_cc': res_cc,
}
# --- Тест на основе длительности (Candelon et al.) ---
def duration_based_test_candelon(breaches: pd.Series):
"""
Проводит тест на независимость пробоев на основе длительности между ними.
(Candelon, Colletaz, Hurlin, Tokpavi, 2011)
"""
breach_indices = breaches[breaches == 1].index
if len(breach_indices) < 2:
return np.nan, np.nan, "Слишком мало пробоев"
# Рассчитываем длительности
first_breach_day_number = breaches.index.get_loc(breach_indices[0]) + 1
durations = np.diff(breaches.index.get_indexer_for(breach_indices))
durations = np.insert(durations, 0, first_breach_day_number)
# Логарифмическая функция правдоподобия для распределения Вейбулла
def log_likelihood_weibull(params, D):
b, c = params[0], params[1]
if b <= 0 or c <= 0: return -np.inf
# Формула из статьи Candelon et al.
ll = -np.sum( (D/b)**c - np.log(c * D**(c-1) / b**c) )
return ll
# Оптимизация для неограниченной модели (c != 1)
res_unrestricted = minimize(
lambda p: -log_likelihood_weibull(p, durations),
x0=[np.mean(durations), 1.0], method='Nelder-Mead'
)
ll_unrestricted = -res_unrestricted.fun
# Оптимизация для ограниченной модели (c = 1, экспоненциальное распределение)
res_restricted = minimize(
lambda p: -log_likelihood_weibull([p[0], 1.0], durations),
x0=[np.mean(durations)], method='Nelder-Mead'
)
ll_restricted = -res_restricted.fun
# Статистика отношения правдоподобия
lr_dur = 2 * (ll_unrestricted - ll_restricted)
p_value = 1 - chi2.cdf(lr_dur, 1)
result = "Отвергнуть H0 (Есть память)" if p_value < 0.05 else "Не отвергать H0"
return lr_dur, p_value, result
T = len(backtesting_df)
expected_breaches = T * 0.01
summary_data = []
portfolio_map = {
'total': 'Весь портфель',
'stocks': 'Акции',
'bonds': 'Облигации',
'fx': 'Валюта'
}
for p_key, p_name in portfolio_map.items():
N = backtesting_df[f'Breach_{p_key}'].sum()
breach_series = backtesting_df[f'Breach_{p_key}']
# Выполняем все тесты
kupiec_res = kupiec_pof_test(T, N, p_star=0.01)
chris_res = christoffersen_test(breach_series, p_star=0.01)
dur_res = duration_based_test_candelon(breach_series)
summary_data.append({
'Портфель': p_name,
'Дней (T)': T,
'Ожидалось пробоев (1%)': f"{expected_breaches:.2f}",
'Факт. пробоев (N)': N,
# Тест Купика (Безусловное покрытие)
'Kupiec (p-val)': f"{kupiec_res[1]:.3f}" if not np.isnan(kupiec_res[1]) else "n/a",
'Kupiec (Вывод)': kupiec_res[2].split(' ')[0],
# Тест Кристофферсена на независимость
'Chris. Indep (p-val)': f"{chris_res['p_val_ind']:.3f}" if not np.isnan(chris_res['p_val_ind']) else "n/a",
'Chris. Indep (Вывод)': chris_res['result_ind'],
# Полный тест Кристофферсена
'Chris. CC (p-val)': f"{chris_res['p_val_cc']:.3f}" if not np.isnan(chris_res['p_val_cc']) else "n/a",
'Chris. CC (Вывод)': chris_res['result_cc'],
# Тест на основе длительности
'Duration Test (p-val)': f"{dur_res[1]:.3f}" if not np.isnan(dur_res[1]) else "n/a",
'Duration Test (Вывод)': dur_res[2],
})
summary_df = pd.DataFrame(summary_data).set_index('Портфель')
print("\nИтоговая таблица валидации VaR 99% за 2024 год (включая расширенные тесты):")
display(summary_df)
Итоговая таблица валидации VaR 99% за 2024 год (включая расширенные тесты):
| Дней (T) | Ожидалось пробоев (1%) | Факт. пробоев (N) | Kupiec (p-val) | Kupiec (Вывод) | Chris. Indep (p-val) | Chris. Indep (Вывод) | Chris. CC (p-val) | Chris. CC (Вывод) | Duration Test (p-val) | Duration Test (Вывод) | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Портфель | |||||||||||
| Весь портфель | 261 | 2.61 | 6 | 0.071 | Не | 0.004 | Отвергнуть H0 (Кластеризация) | 0.003 | Отвергнуть H0 | 0.108 | Не отвергать H0 |
| Акции | 261 | 2.61 | 12 | 0.000 | Отвергнуть | 0.105 | Не отвергать H0 | 0.000 | Отвергнуть H0 | 0.172 | Не отвергать H0 |
| Облигации | 261 | 2.61 | 70 | 0.000 | Отвергнуть | 0.000 | Отвергнуть H0 (Кластеризация) | 0.000 | Отвергнуть H0 | 0.777 | Не отвергать H0 |
| Валюта | 261 | 2.61 | 7 | 0.024 | Отвергнуть | 0.166 | Не отвергать H0 | 0.030 | Отвергнуть H0 | 0.087 | Не отвергать H0 |
csv_path = "backtesting_summary_2024_2000_10.csv"
summary_df.to_csv(csv_path, index=True, encoding="utf-8-sig")
import pandas as pd
print('Результаты при запуске 2000 траекторий')
display(pd.read_csv('data/results/backtesting_summary_2024_2000_10.csv'))
print('\nРезультаты при запуске 10000 траекторий')
display(pd.read_csv('data/results/backtesting_summary_2024_10000_10.csv'))
Результаты при запуске 2000 траекторий
| Портфель | Дней (T) | Ожидалось пробоев (1%) | Факт. пробоев (N) | Kupiec (p-val) | Kupiec (Вывод) | Chris. Indep (p-val) | Chris. Indep (Вывод) | Chris. CC (p-val) | Chris. CC (Вывод) | Duration Test (p-val) | Duration Test (Вывод) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Весь портфель | 261 | 2.61 | 6 | 0.071 | Не | 0.004 | Отвергнуть H0 (Кластеризация) | 0.003 | Отвергнуть H0 | 0.108 | Не отвергать H0 |
| 1 | Акции | 261 | 2.61 | 12 | 0.000 | Отвергнуть | 0.105 | Не отвергать H0 | 0.000 | Отвергнуть H0 | 0.172 | Не отвергать H0 |
| 2 | Облигации | 261 | 2.61 | 70 | 0.000 | Отвергнуть | 0.000 | Отвергнуть H0 (Кластеризация) | 0.000 | Отвергнуть H0 | 0.777 | Не отвергать H0 |
| 3 | Валюта | 261 | 2.61 | 7 | 0.024 | Отвергнуть | 0.166 | Не отвергать H0 | 0.030 | Отвергнуть H0 | 0.087 | Не отвергать H0 |
Результаты при запуске 10000 траекторий
| Портфель | Дней (T) | Ожидалось пробоев (1%) | Факт. пробоев (N) | Kupiec (p-val) | Kupiec (Вывод) | Chris. Indep (p-val) | Chris. Indep (Вывод) | Chris. CC (p-val) | Chris. CC (Вывод) | Duration Test (p-val) | Duration Test (Вывод) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Весь портфель | 261 | 2.61 | 6 | 0.071 | Не | 0.004 | Отвергнуть H0 (Кластеризация) | 0.003 | Отвергнуть H0 | 0.108 | Не отвергать H0 |
| 1 | Акции | 261 | 2.61 | 11 | 0.000 | Отвергнуть | 0.072 | Не отвергать H0 | 0.000 | Отвергнуть H0 | 0.244 | Не отвергать H0 |
| 2 | Облигации | 261 | 2.61 | 70 | 0.000 | Отвергнуть | 0.000 | Отвергнуть H0 (Кластеризация) | 0.000 | Отвергнуть H0 | 0.777 | Не отвергать H0 |
| 3 | Валюта | 261 | 2.61 | 6 | 0.071 | Не | 0.114 | Не отвергать H0 | 0.056 | Не отвергать H0 | 0.161 | Не отвергать H0 |